diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f562ef89b..cc5a3c110 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -784,6 +784,9 @@ "clear": { "message": "Clear" }, + "copySeed": { + "message": "Copy Seed" + }, "failedToSend": { "message": "Failed to send to some recipients. Check your network connection." @@ -1682,8 +1685,8 @@ }, "copiedMnemonic": { - "message": "Copied mnemonic to clipboard", - "description": "A toast message telling the user that the mnemonic was copied" + "message": "Copied seed to clipboard", + "description": "A toast message telling the user that the mnemonic seed was copied" }, "passwordViewTitle": { diff --git a/background.html b/background.html index 295eaef6e..eaca125b9 100644 --- a/background.html +++ b/background.html @@ -229,6 +229,20 @@ + + diff --git a/js/background.js b/js/background.js index 831ad3044..f04ce680e 100644 --- a/js/background.js +++ b/js/background.js @@ -608,6 +608,14 @@ } }); + Whisper.events.on('showSeedDialog', async () => { + const manager = await getAccountManager(); + if (appView && manager) { + const seed = manager.getCurrentMnemonic(); + appView.showSeedDialog(seed); + } + }); + Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => { try { const conversation = ConversationController.get(pubKey); diff --git a/js/views/app_view.js b/js/views/app_view.js index fcf865a39..5156ac5a8 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -191,5 +191,9 @@ const dialog = Whisper.getPasswordDialogView(type, resolve, reject); this.el.append(dialog.el); }, + showSeedDialog(seed) { + const dialog = new Whisper.SeedDialogView({ seed }); + this.el.append(dialog.el); + }, }); })(); diff --git a/js/views/seed_dialog_view.js b/js/views/seed_dialog_view.js new file mode 100644 index 000000000..1d5b394ec --- /dev/null +++ b/js/views/seed_dialog_view.js @@ -0,0 +1,56 @@ +/* global Whisper, i18n */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + Whisper.SeedDialogView = Whisper.View.extend({ + className: 'loki-dialog seed-dialog modal', + templateName: 'seed-dialog', + initialize(options = {}) { + this.okText = options.okText || i18n('ok'); + this.copyText = options.copyText || i18n('copySeed'); + this.seed = options.seed || '-'; + + this.render(); + }, + events: { + keyup: 'onKeyup', + 'click .ok': 'ok', + 'click .copy-seed': 'copySeed', + }, + render_attributes() { + return { + seed: this.seed, + ok: this.okText, + copyText: this.copyText, + }; + }, + ok() { + this.remove(); + }, + copySeed() { + window.clipboard.writeText(this.seed); + + const toast = new Whisper.MessageToastView({ + message: i18n('copiedMnemonic'), + }); + toast.$el.appendTo(this.$el); + toast.render(); + }, + onKeyup(event) { + switch (event.key) { + case 'Enter': + case 'Escape': + case 'Esc': + this.ok(); + break; + default: + return; + } + event.preventDefault(); + }, + }); +})(); diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index 4b93aac15..5a9599aac 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -58,7 +58,7 @@ 'keyup #password': 'onPasswordChange', 'keyup #password-confirmation': 'onValidatePassword', }, - async register(mnemonic) { + async register(mnemonic, language) { // Make sure the password is valid if (this.validatePassword()) { this.showToast('Invalid password'); @@ -71,7 +71,7 @@ await window.setPassword(input); await this.accountManager.registerSingleDevice( mnemonic, - this.$('#mnemonic-language').val(), + language, this.$('#display-name').val() ); this.$el.trigger('openInbox'); @@ -84,14 +84,16 @@ }, registerWithoutMnemonic() { const mnemonic = this.$('#mnemonic-display').text(); - this.register(mnemonic); + const language = this.$('#mnemonic-display-language').val(); + this.register(mnemonic, language); }, registerWithMnemonic() { const mnemonic = this.$('#mnemonic').val(); + const language = this.$('#mnemonic-language').val(); if (!mnemonic) { this.log('Please provide a mnemonic word list'); } else { - this.register(mnemonic); + this.register(mnemonic, language); } }, onChangeMnemonic() { diff --git a/libloki/mnemonic.js b/libloki/mnemonic.js index 33bfd3511..05bbb2f69 100644 --- a/libloki/mnemonic.js +++ b/libloki/mnemonic.js @@ -123,7 +123,7 @@ function mn_decode(str, wordset_name) { ); } if (w1 === -1 || w2 === -1 || w3 === -1) { - throw MnemonicError('invalid word in mnemonic'); + throw new MnemonicError('invalid word in mnemonic'); } var x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n); if (x % n != w1) diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 7795a4a61..99bad2969 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -72,6 +72,7 @@ return this.queueTask(() => generateKeypair().then(async identityKeyPair => { return createAccount(identityKeyPair) + .then(() => this.saveMnemonic(mnemonic)) .then(clearSessionsAndPreKeys) .then(generateKeys) .then(confirmKeys) @@ -451,6 +452,12 @@ const hex = StringView.arrayBufferToHex(keys.privKey); return mnemonic.mn_encode(hex, language); }, + getCurrentMnemonic() { + return textsecure.storage.get('mnemonic'); + }, + saveMnemonic(mnemonic) { + return textsecure.storage.put('mnemonic', mnemonic); + }, async registrationDone(number, profileName) { window.log.info('registration done'); diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 4c23e6bf5..b44290680 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -425,6 +425,17 @@ } } +.seed-dialog { + .title { + font-weight: bold; + } + + .seed { + padding: 20px 0; + font-style: oblique; + } +} + .permissions-popup, .debug-log-window { .modal { diff --git a/test/index.html b/test/index.html index 49c05c888..0152ccafe 100644 --- a/test/index.html +++ b/test/index.html @@ -404,6 +404,7 @@ +