diff --git a/src/UI/UIWindowEmailConfirmationRequired.js b/src/UI/UIWindowEmailConfirmationRequired.js index 4c472f7f3..61defb1bc 100644 --- a/src/UI/UIWindowEmailConfirmationRequired.js +++ b/src/UI/UIWindowEmailConfirmationRequired.js @@ -26,7 +26,8 @@ function UIWindowEmailConfirmationRequired(options){ let final_code = ''; let is_checking_code = false; - const submit_btn_txt = 'Confirm Email' + const submit_btn_txt = 'Confirm Email'; + let h = ''; h += `
×
`; h += `
`; diff --git a/src/UI/UIWindowLogin.js b/src/UI/UIWindowLogin.js index cafd17928..e3e78ada8 100644 --- a/src/UI/UIWindowLogin.js +++ b/src/UI/UIWindowLogin.js @@ -20,6 +20,7 @@ import UIWindow from './UIWindow.js' import UIWindowSignup from './UIWindowSignup.js' import UIWindowRecoverPassword from './UIWindowRecoverPassword.js' +import UIWindowVerificationCode from './UIWindowVerificationCode.js'; async function UIWindowLogin(options){ options = options ?? {}; @@ -163,7 +164,12 @@ async function UIWindowLogin(options){ headers: headers, contentType: "application/json", data: data, - success: function (data){ + success: async function (data){ + if ( data.next_step === 'otp' ) { + const value = await UIWindowVerificationCode(); + console.log('got value', value); + } + window.update_auth_data(data.token, data.user); if(options.reload_on_success){ @@ -234,4 +240,4 @@ async function UIWindowLogin(options){ }) } -export default UIWindowLogin \ No newline at end of file +export default UIWindowLogin diff --git a/src/UI/UIWindowVerificationCode.js b/src/UI/UIWindowVerificationCode.js new file mode 100644 index 000000000..2d82bd193 --- /dev/null +++ b/src/UI/UIWindowVerificationCode.js @@ -0,0 +1,181 @@ +import TeePromise from "../util/TeePromise.js"; +import UIWindow from "./UIWindow.js"; + +const UIWindowVerificationCode = async function UIWindowVerificationCode ( options ) { + options = options ?? {}; + let final_code = ''; + let is_checking_code = false; + + const html_title = i18n(options.title_key || 'confirm_code_generic_title'); + const html_confirm = i18n(options.confirm_key || 'confirm_code_generic_confirm'); + const html_instruction = i18n(options.instruction_key || 'confirm_code_generic_instruction'); + const submit_btn_txt = i18n(options.submit_btn_key || 'confirm_code_generic_submit'); + + let h = ''; + h += `
×
`; + h += `
`; + h += `

${ html_title }

`; + h += `
`; + h += `

${ html_instruction }

`; + h += `
`; + h += `
+ + + + - + + + +
`; + h += ``; + h += `
`; + h += `
`; + h += `what is this text`; + h += `
`; + h += `
`; + + const el_window = await UIWindow({ + title: null, + icon: null, + uid: null, + is_dir: false, + body_content: h, + has_head: false, + selectable_body: false, + draggable_body: true, + allow_context_menu: false, + is_draggable: options.is_draggable ?? true, + is_droppable: false, + is_resizable: false, + stay_on_top: options.stay_on_top ?? false, + allow_native_ctxmenu: true, + allow_user_select: true, + backdrop: true, + width: 390, + dominant: true, + onAppend: function(el_window){ + $(el_window).find('.digit-input').first().focus(); + }, + window_class: 'window-item-properties', + window_css:{ + height: 'initial', + }, + body_css: { + padding: '30px', + width: 'initial', + height: 'initial', + 'background-color': 'rgb(247 251 255)', + 'backdrop-filter': 'blur(3px)', + } + }); + + + const p = new TeePromise(); + + $(el_window).find('.digit-input').first().focus(); + + $(el_window).find('.code-confirm-btn').on('click submit', function(e){ + e.preventDefault(); + e.stopPropagation(); + + $(el_window).find('.code-confirm-btn').prop('disabled', true); + $(el_window).find('.error').hide(); + + // Check if already checking code to prevent multiple requests + if(is_checking_code) + return; + // Confirm button + is_checking_code = true; + + // set animation + $(el_window).find('.code-confirm-btn').html(`circle anim`); + + setTimeout(() => { + console.log('final code', final_code); + p.resolve(final_code); + }, 1000); + }) + + // Elements + const numberCodeForm = document.querySelector('[data-number-code-form]'); + const numberCodeInputs = [...numberCodeForm.querySelectorAll('[data-number-code-input]')]; + + // Event listeners + numberCodeForm.addEventListener('input', ({ target }) => { + if(!target.value.length) { return target.value = null; } + const inputLength = target.value.length; + let currentIndex = Number(target.dataset.numberCodeInput); + if(inputLength === 2){ + const inputValues = target.value.split(''); + target.value = inputValues[0]; + } + else if (inputLength > 1) { + const inputValues = target.value.split(''); + + inputValues.forEach((value, valueIndex) => { + const nextValueIndex = currentIndex + valueIndex; + + if (nextValueIndex >= numberCodeInputs.length) { return; } + + numberCodeInputs[nextValueIndex].value = value; + }); + currentIndex += inputValues.length - 2; + } + + const nextIndex = currentIndex + 1; + + if (nextIndex < numberCodeInputs.length) { + numberCodeInputs[nextIndex].focus(); + } + + // Concatenate all inputs into one string to create the final code + final_code = ''; + for(let i=0; i< numberCodeInputs.length; i++){ + final_code += numberCodeInputs[i].value; + } + // Automatically submit if 6 digits entered + if(final_code.length === 6){ + $(el_window).find('.code-confirm-btn').prop('disabled', false); + $(el_window).find('.code-confirm-btn').trigger('click'); + } + }); + + numberCodeForm.addEventListener('keydown', (e) => { + const { code, target } = e; + + const currentIndex = Number(target.dataset.numberCodeInput); + const previousIndex = currentIndex - 1; + const nextIndex = currentIndex + 1; + + const hasPreviousIndex = previousIndex >= 0; + const hasNextIndex = nextIndex <= numberCodeInputs.length - 1 + + switch (code) { + case 'ArrowLeft': + case 'ArrowUp': + if (hasPreviousIndex) { + numberCodeInputs[previousIndex].focus(); + } + e.preventDefault(); + break; + + case 'ArrowRight': + case 'ArrowDown': + if (hasNextIndex) { + numberCodeInputs[nextIndex].focus(); + } + e.preventDefault(); + break; + case 'Backspace': + if (!e.target.value.length && hasPreviousIndex) { + numberCodeInputs[previousIndex].value = null; + numberCodeInputs[previousIndex].focus(); + } + break; + default: + break; + } + }); +} + +export default UIWindowVerificationCode; diff --git a/src/css/style.css b/src/css/style.css index aa162e538..cd0d35a34 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1946,6 +1946,10 @@ label { cursor: pointer; } +.send-conf-code { + cursor: pointer; +} + .email-confirm-code-hyphen { display: inline-block; width: 14%; @@ -1954,10 +1958,22 @@ label { font-weight: 300; } +.confirm-code-hyphen { + display: inline-block; + width: 14%; + text-align: center; + font-size: 40px; + font-weight: 300; +} + .send-conf-email:hover, .conf-email-log-out:hover { text-decoration: underline; } +.send-conf-code:hover { + text-decoration: underline; +} + .remove-permission-link, .disassociate-website-link { cursor: pointer; color: red; diff --git a/src/i18n/translations/en.js b/src/i18n/translations/en.js index 7e46af1a7..37601523f 100644 --- a/src/i18n/translations/en.js +++ b/src/i18n/translations/en.js @@ -47,6 +47,8 @@ const en = { color: 'Color', hue: 'Hue', confirm_account_for_free_referral_storage_c2a: 'Create an account and confirm your email address to receive 1 GB of free storage. Your friend will get 1 GB of free storage too.', + confirm_code_generic_title: "Enter Confirmation Code", + confirm_code_2fa_title: "Enter 2FA Code", confirm_delete_multiple_items: 'Are you sure you want to permanently delete these items?', confirm_delete_single_item: 'Do you want to permanently delete this item?', confirm_open_apps_log_out: 'You have open apps. Are you sure you want to log out?', diff --git a/src/util/TeePromise.js b/src/util/TeePromise.js new file mode 100644 index 000000000..94f9b9d6f --- /dev/null +++ b/src/util/TeePromise.js @@ -0,0 +1,43 @@ +export default class TeePromise { + static STATUS_PENDING = {}; + static STATUS_RUNNING = {}; + static STATUS_DONE = {}; + constructor () { + this.status_ = this.constructor.STATUS_PENDING; + this.donePromise = new Promise((resolve, reject) => { + this.doneResolve = resolve; + this.doneReject = reject; + }); + } + get status () { + return this.status_; + } + set status (status) { + this.status_ = status; + if ( status === this.constructor.STATUS_DONE ) { + this.doneResolve(); + } + } + resolve (value) { + this.status_ = this.constructor.STATUS_DONE; + this.doneResolve(value); + } + awaitDone () { + return this.donePromise; + } + then (fn, rfn) { + return this.donePromise.then(fn, rfn); + } + + reject (err) { + this.status_ = this.constructor.STATUS_DONE; + this.doneReject(err); + } + + /** + * @deprecated use then() instead + */ + onComplete(fn) { + return this.then(fn); + } +}