decred.org/dcrdex@v1.0.5/client/webserver/site/src/js/init.ts (about) 1 import Doc from './doc' 2 import BasePage from './basepage' 3 import { postJSON } from './http' 4 import * as intl from './locales' 5 import { 6 bind as bindForm, 7 slideSwap 8 } from './forms' 9 import { Wave } from './charts' 10 import { 11 app, 12 PageElement, 13 SupportedAsset, 14 User, 15 WalletInfo, 16 WalletDefinition, 17 ConfigOption, 18 APIResponse 19 } from './registry' 20 21 interface InitResponse extends APIResponse { 22 hosts: string[] 23 mnemonic?: string 24 } 25 26 /* 27 * InitPage is the page handler for the /init view. InitPage is essentially a 28 * form handler. There are no non-form elements on /init. InitPage additionally 29 * has a role caching the initialization password. A couple of notes about 30 * InitPage. 31 * 1) There is no going backwards. Once you set a password, you can't go back 32 * to the password form. If you refresh, you won't end up on /init, so 33 * won't have access to the QuickConfigForm or SeedBackupForm . Once you 34 * submit your auto-config choices, you can't change them. This has 35 * implications for coding and UI. There are no "go back" or "close form" 36 * elements. 37 * 2) The user can preclude auto-config and seed backup by clicking an 38 * available header link after password init, e.g. Wallets, in the page 39 * header. NOTE: Regardless of what the user does after setting the app 40 * pass, they will receive a notification reminding them to back up their 41 * seed. Perhaps it would be better to somehow delay that message until 42 * they choose to ignore the seed backup dialog, but having more reminders 43 * is also okay. 44 */ 45 export default class InitPage extends BasePage { 46 body: HTMLElement 47 page: Record<string, PageElement> 48 initForm: AppInitForm 49 quickConfigForm: QuickConfigForm 50 seedBackupForm: SeedBackupForm 51 mnemonic?: string 52 53 constructor (body: HTMLElement) { 54 super() 55 this.body = body 56 const page = this.page = Doc.idDescendants(body) 57 this.initForm = new AppInitForm(page.appPWForm, (pw: string, hosts: string[], mnemonic?: string) => { this.appInited(pw, hosts, mnemonic) }) 58 this.quickConfigForm = new QuickConfigForm(page.quickConfigForm, () => this.quickConfigDone()) 59 this.seedBackupForm = new SeedBackupForm(page.seedBackupForm, () => this.seedBackedUp()) 60 } 61 62 async appInited (pw: string, hosts: string[], mnemonic?: string) { 63 this.mnemonic = mnemonic 64 const page = this.page 65 await this.quickConfigForm.update(pw, hosts) 66 if (mnemonic) this.seedBackupForm.update(mnemonic) 67 slideSwap(page.appPWForm, page.quickConfigForm) 68 } 69 70 quickConfigDone () { 71 if (!this.mnemonic) app().loadPage('wallets') 72 else slideSwap(this.page.quickConfigForm, this.page.seedBackupForm) 73 } 74 75 seedBackedUp () { 76 app().loadPage('wallets') 77 } 78 } 79 80 /* 81 * The AppInitForm handles the form that sets the app password, accepts an 82 * optional seed, and initializes the app. 83 */ 84 class AppInitForm { 85 form: PageElement 86 page: Record<string, PageElement> 87 success: (pw: string, hosts: string[], mnemonic?: string) => void 88 89 constructor (form: PageElement, success: (pw: string, hosts: string[], mnemonic?: string) => void) { 90 this.form = form 91 this.success = success 92 const page = this.page = Doc.idDescendants(form) 93 bindForm(form, page.appPWSubmit, () => this.setAppPass()) 94 bindForm(form, page.toggleSeedInput, () => { 95 if (Doc.isHidden(page.seedInputBox)) { 96 page.toggleSeedInputIcon.classList.remove('ico-plus') 97 page.toggleSeedInputIcon.classList.add('ico-minus') 98 Doc.show(page.seedInputBox) 99 } else { 100 page.toggleSeedInputIcon.classList.remove('ico-minus') 101 page.toggleSeedInputIcon.classList.add('ico-plus') 102 Doc.hide(page.seedInputBox) 103 } 104 }) 105 } 106 107 /* Set the application password. Attached to form submission. */ 108 async setAppPass () { 109 const page = this.page 110 Doc.hide(page.appPWErrMsg) 111 const pw = page.appPW.value || '' 112 const pwAgain = page.appPWAgain.value 113 if (pw === '') { 114 page.appPWErrMsg.textContent = intl.prep(intl.ID_NO_PASS_ERROR_MSG) 115 Doc.show(page.appPWErrMsg) 116 return 117 } 118 if (pw !== pwAgain) { 119 page.appPWErrMsg.textContent = intl.prep(intl.ID_PASSWORD_NOT_MATCH) 120 Doc.show(page.appPWErrMsg) 121 return 122 } 123 124 page.appPW.value = '' 125 page.appPWAgain.value = '' 126 const loaded = app().loading(this.form) 127 // const seed = page.seedInput.value?.replace(/\s+/g, '') // strip whitespace 128 const seed = page.seedInput.value ?? '' 129 const res: InitResponse = await postJSON('/api/init', { 130 pass: pw, 131 seed: seed 132 }) 133 loaded() 134 if (!app().checkResponse(res)) { 135 page.appPWErrMsg.textContent = res.msg 136 Doc.show(page.appPWErrMsg) 137 return 138 } 139 this.success(pw, res.hosts, res.mnemonic) 140 } 141 } 142 143 // HostConfigRow is used by the QuickConfigForm to track the user's choices. 144 interface HostConfigRow { 145 host: string 146 checkbox: HTMLInputElement 147 } 148 149 // WalletConfigRow is used by the QuickConfigForm to track the user's choices. 150 interface WalletConfigRow { 151 asset: SupportedAsset 152 type: string 153 checkbox: HTMLInputElement 154 } 155 156 let rowIDCounter = 0 157 158 /* 159 * QuickConfigForm handles the form that allows users to quickly configure 160 * view-only servers and native wallets (that don't require any configuration). 161 */ 162 class QuickConfigForm { 163 page: Record<string, PageElement> 164 form: PageElement 165 servers: HostConfigRow[] 166 wallets: WalletConfigRow[] 167 pw: string 168 success: () => void 169 170 constructor (form: PageElement, success: () => void) { 171 this.form = form 172 this.success = success 173 const page = this.page = Doc.idDescendants(form) 174 Doc.cleanTemplates(page.qcServerTmpl, page.qcWalletTmpl) 175 bindForm(form, page.quickConfigSubmit, () => { this.submit() }) 176 bindForm(form, page.qcErrAck, () => { this.success() }) 177 } 178 179 async update (pw: string, hosts: string[]) { 180 this.pw = pw 181 const page = this.page 182 183 this.servers = [] 184 for (const host of hosts) { 185 const row = page.qcServerTmpl.cloneNode(true) as PageElement 186 page.qcServersBox.appendChild(row) 187 const tmpl = Doc.parseTemplate(row) 188 rowIDCounter++ 189 const rowID = `qcsrow${rowIDCounter}` 190 row.htmlFor = rowID 191 tmpl.checkbox.id = rowID 192 tmpl.host.textContent = host 193 this.servers.push({ host, checkbox: tmpl.checkbox as HTMLInputElement }) 194 } 195 196 const u = await app().fetchUser() as User 197 this.wallets = [] 198 for (const a of Object.values(u.assets)) { 199 if (a.token) continue 200 const winfo = a.info as WalletInfo 201 let autoConfigurable: WalletDefinition | null = null 202 for (const wDef of winfo.availablewallets) { 203 if (!wDef.seeded) continue 204 if (wDef.configopts && wDef.configopts.some((opt: ConfigOption) => opt.required)) continue 205 autoConfigurable = wDef 206 break 207 } 208 if (!autoConfigurable) continue 209 const row = page.qcWalletTmpl.cloneNode(true) as PageElement 210 page.qcWalletsBox.appendChild(row) 211 const tmpl = Doc.parseTemplate(row) 212 rowIDCounter++ 213 const rowID = `qcwrow${rowIDCounter}` 214 row.htmlFor = rowID 215 tmpl.checkbox.id = rowID 216 tmpl.icon.src = Doc.logoPath(a.symbol) 217 tmpl.name.textContent = a.name 218 this.wallets.push({ 219 asset: a, 220 type: autoConfigurable.type, 221 checkbox: tmpl.checkbox as HTMLInputElement 222 }) 223 } 224 } 225 226 async submit () { 227 const [failedHosts, failedWallets]: [string[], string[]] = [[], []] 228 const ani = new Wave(this.form, { backgroundColor: true, message: '...' }) 229 ani.opts.message = intl.prep(intl.ID_ADDING_SERVERS) 230 const connectServer = async (srvRow: HostConfigRow) => { 231 if (!srvRow.checkbox.checked) return 232 const req = { 233 addr: srvRow.host, 234 appPW: this.pw 235 } 236 const res = await postJSON('/api/adddex', req) // DRAFT NOTE: ignore errors ok? 237 if (!app().checkResponse(res)) failedHosts.push(srvRow.host) 238 } 239 await Promise.all(this.servers.map(connectServer)) 240 241 ani.opts.message = intl.prep(intl.ID_CREATING_WALLETS) 242 const createWallet = async (walletRow: WalletConfigRow) => { 243 const { asset: a, type, checkbox } = walletRow 244 if (!checkbox.checked) return 245 const config: Record<string, string> = {} 246 const walletDef = app().walletDefinition(a.id, type) 247 for (const opt of (walletDef.configopts ?? [])) { 248 if (!opt.default) continue 249 if (opt.isboolean) { 250 config[opt.key] = opt.default ? '1' : '0' 251 continue 252 } 253 if (opt.repeatable && config[opt.key]) config[opt.key] += opt.repeatable + opt.default 254 else config[opt.key] = String(opt.default) 255 } 256 const createForm = { 257 assetID: a.id, 258 appPass: this.pw, 259 config: config, 260 walletType: type 261 } 262 const res = await postJSON('/api/newwallet', createForm) 263 if (!app().checkResponse(res)) failedWallets.push(a.name) 264 } 265 await Promise.all(this.wallets.map(createWallet)) 266 267 ani.stop() 268 await app().fetchUser() // Calls updateMenuItemsDisplay internally 269 if (failedWallets.length + failedHosts.length === 0) return this.success() 270 271 const page = this.page 272 Doc.hide(page.qcChoices) 273 Doc.show(page.qcErrors) 274 275 if (failedHosts.length) { 276 for (const host of failedHosts) { 277 page.qcServerErrorList.appendChild(document.createTextNode(host)) 278 page.qcServerErrorList.appendChild(document.createElement('br')) 279 } 280 } else Doc.hide(page.qcServerErrors) 281 282 if (failedWallets.length) { 283 for (const name of failedWallets) { 284 page.qcWalletErrorList.appendChild(document.createTextNode(name)) 285 page.qcWalletErrorList.appendChild(document.createElement('br')) 286 } 287 } else Doc.hide(page.qcWalletErrors) 288 } 289 } 290 291 /* 292 * SeedBackupForm handles the form that allows the user to back up their seed 293 * during initialization. 294 */ 295 class SeedBackupForm { 296 form: PageElement 297 page: Record<string, PageElement> 298 mnemonic: string 299 300 constructor (form: PageElement, success: () => void) { 301 this.form = form 302 const page = this.page = Doc.idDescendants(form) 303 bindForm(form, page.seedAck, () => success()) 304 bindForm(form, page.showSeed, () => this.showSeed()) 305 } 306 307 update (mnemonic: string) { 308 this.mnemonic = mnemonic 309 } 310 311 showSeed () { 312 const page = this.page 313 page.mnemonic.textContent = this.mnemonic // `${words.slice(0, 5).join(' ')}\n${words.slice(5, 10).join(' ')}\n${words.slice(10,15).join(' ')}` 314 Doc.hide(page.sbWanna) 315 Doc.show(page.sbSeed) 316 } 317 }