diff --git a/ember-primitives/src/components/one-time-password/input.gts b/ember-primitives/src/components/one-time-password/input.gts index 98d0b9abe..f118f5a04 100644 --- a/ember-primitives/src/components/one-time-password/input.gts +++ b/ember-primitives/src/components/one-time-password/input.gts @@ -21,6 +21,8 @@ function labelFor(inputIndex: number, labelFn: undefined | ((index: number) => s let waiter = buildWaiter('ember-primitives:OTPInput:handleChange'); +let isFirst = (i: number) => i === 0; + const Fields: TOC<{ /** * Any attributes passed to this component will be applied to each input. @@ -40,6 +42,7 @@ const Fields: TOC<{ type='text' inputmode='numeric' ...attributes + autocomplete={{if (isFirst i) "one-time-code"}} {{on 'input' autoAdvance}} {{on 'input' @handleChange}} {{on 'keydown' handleNavigation}} diff --git a/ember-primitives/src/components/one-time-password/otp.gts b/ember-primitives/src/components/one-time-password/otp.gts index a193ccdc2..ee17bc7f0 100644 --- a/ember-primitives/src/components/one-time-password/otp.gts +++ b/ember-primitives/src/components/one-time-password/otp.gts @@ -1,18 +1,23 @@ +/** eslint-disable @typescript-eslint/no-explicit-any */ +// any disabled for this file, because not all browsers have the APIs used +import Component from '@glimmer/component'; import { assert } from '@ember/debug'; import { fn, hash } from '@ember/helper'; import { on } from '@ember/modifier'; import { buildWaiter } from '@ember/test-waiters'; +import { resource, resourceFactory } from 'ember-resources'; + import { Reset, Submit } from './buttons'; import { OTPInput } from './input'; -import type { TOC } from '@ember/component/template-only'; import type { WithBoundArgs } from '@glint/template'; let waiter = buildWaiter('ember-primitives:OTP:handleAutoSubmitAttempt'); -const handleFormSubmit = (submit: (data: { code: string }) => void, event: SubmitEvent) => { +const handleFormSubmit = (submit: (data: { code: string }) => void, signal: undefined | { abort: () => void }, event: SubmitEvent) => { event.preventDefault(); + signal?.abort?.(); assert( '[BUG]: handleFormSubmit was not attached to a form. Please open an issue.', @@ -63,7 +68,45 @@ function handleChange( form.requestSubmit(); } -export const OTP: TOC<{ +const GetOTP = resourceFactory(( receiveOTP: (code: string) => void ) => resource(({ on }) => { + let ac = new AbortController(); + + on.cleanup(() => ac.abort()); + + // Not All browsers support this. + // Mainly, the only browsers that do support it are ones + // that also have a corresponding phone ecosystem. + // (Chrome and Safari support this, but FireFox does not) + let hasOTPCredential = "OTPCredential" in window; + + if (!hasOTPCredential) { + return; + } + + navigator.credentials + .get({ + otp: { transport: ["sms"] }, + signal: ac.signal, + } as any) + .then((otp) => { + if (!otp) return; + // there is also a type property, but it's always the same "otp" string + receiveOTP(( otp as any).code); + }) + .catch((err) => { + console.error(err); + }); + + + return { + // To be called if the form is submitted, + // so we stop waiting for credentials. + abort: () => ac.abort(), + }; +})); + + +export class OTP extends Component<{ /** * The overall OTP Input is in its own form. * Modern UI/UX Patterns usually have this sort of field @@ -114,16 +157,27 @@ export const OTP: TOC<{ }, ]; }; -}> = ; +}> { + handleOTP = (code) => { + // TODO: + // 1. ensure that the fields have been populated with the value + // 2. do form.requestSubmit() + } + + +} +