@oxy.so/services 11.3.1 → 11.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/lib/commonjs/ui/components/signIn/OxyEmailAccountChoice.js +113 -0
  2. package/lib/commonjs/ui/components/signIn/OxyEmailAccountChoice.js.map +1 -0
  3. package/lib/commonjs/ui/components/signIn/OxySignInPanel.js +70 -3
  4. package/lib/commonjs/ui/components/signIn/OxySignInPanel.js.map +1 -1
  5. package/lib/commonjs/ui/components/signIn/accountFlowParts.js +2 -0
  6. package/lib/commonjs/ui/components/signIn/accountFlowParts.js.map +1 -1
  7. package/lib/module/ui/components/signIn/OxyEmailAccountChoice.js +110 -0
  8. package/lib/module/ui/components/signIn/OxyEmailAccountChoice.js.map +1 -0
  9. package/lib/module/ui/components/signIn/OxySignInPanel.js +71 -4
  10. package/lib/module/ui/components/signIn/OxySignInPanel.js.map +1 -1
  11. package/lib/module/ui/components/signIn/accountFlowParts.js +2 -0
  12. package/lib/module/ui/components/signIn/accountFlowParts.js.map +1 -1
  13. package/lib/typescript/commonjs/ui/components/signIn/OxyEmailAccountChoice.d.ts +20 -0
  14. package/lib/typescript/commonjs/ui/components/signIn/OxyEmailAccountChoice.d.ts.map +1 -0
  15. package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts +2 -0
  16. package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
  17. package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
  18. package/lib/typescript/module/ui/components/signIn/OxyEmailAccountChoice.d.ts +20 -0
  19. package/lib/typescript/module/ui/components/signIn/OxyEmailAccountChoice.d.ts.map +1 -0
  20. package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts +2 -0
  21. package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
  22. package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
  23. package/package.json +4 -4
  24. package/src/ui/components/signIn/OxyEmailAccountChoice.tsx +116 -0
  25. package/src/ui/components/signIn/OxySignInPanel.tsx +89 -4
  26. package/src/ui/components/signIn/accountFlowParts.tsx +2 -0
@@ -16,6 +16,8 @@
16
16
  * (the start response does not say which, by design); meanwhile
17
17
  * the screen asks whether the link was opened in this browser,
18
18
  * and signs in when it was.
19
+ * choose the email holds several accounts, and the code or the link just
20
+ * proved it: which one signs in (a username never gets here).
19
21
  * password the alternative for an account that has one.
20
22
  * second-factor the authenticator's code, or a backup code, when the account has one.
21
23
  *
@@ -39,9 +41,11 @@ import {
39
41
  EMAIL_SIGNIN_LONG_CODE_LENGTH,
40
42
  SIGN_IN_ERROR_CODES,
41
43
  TOTP_DIGITS,
44
+ isEmailSignInChooseAccount,
42
45
  isSecondFactorRequired,
46
+ type EmailSignInAccountOption,
47
+ type EmailSignInStepResult,
43
48
  type LoginResult,
44
- type SignInStepResult,
45
49
  } from '@oxy.so/contracts';
46
50
  import { useQueryClient } from '@tanstack/react-query';
47
51
  import { useOxy } from '../../context/OxyContext';
@@ -56,6 +60,7 @@ import { SIGN_IN_SLIDES } from './artwork';
56
60
  import { InlineCommonsQr } from './InlineCommonsQr';
57
61
  import { OxyAccountPicker } from './OxyAccountPicker';
58
62
  import { OxyAuthScreen, OxyAuthScreenHeader, OxyAuthSplit, OxyAuthTerms } from './OxyAuthScreen';
63
+ import { OxyEmailAccountChoice } from './OxyEmailAccountChoice';
59
64
  import {
60
65
  AccountFlowAction,
61
66
  AccountFlowCodeField,
@@ -94,6 +99,13 @@ const SIGN_IN_FLOW_KEY = 'signin';
94
99
  type Step =
95
100
  | { name: 'start' }
96
101
  | { name: 'check-email'; identifier: string; requestId: string; requestSecret: string }
102
+ | {
103
+ name: 'choose';
104
+ identifier: string;
105
+ requestId: string;
106
+ requestSecret: string;
107
+ accounts: EmailSignInAccountOption[];
108
+ }
97
109
  | { name: 'password'; identifier: string }
98
110
  | { name: 'second-factor'; challengeId: string; identifier: string };
99
111
 
@@ -153,6 +165,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
153
165
  const [useBackupCode, setUseBackupCode] = useState(saved?.useBackupCode ?? false);
154
166
  const [longCode, setLongCode] = useState(saved?.longCode ?? false);
155
167
  const [pending, setPending] = useState(false);
168
+ const [choosingId, setChoosingId] = useState<string | null>(null);
156
169
  const [error, setError] = useState<string | null>(null);
157
170
  const [notice, setNotice] = useState<string | null>(null);
158
171
  const [rateLimitSeconds, setRateLimitSeconds] = useState(0);
@@ -225,8 +238,25 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
225
238
  // One sign-in ends once: the typed code and the opened link can race.
226
239
  const finishingRef = useRef(false);
227
240
  const finish = useCallback(
228
- async (result: SignInStepResult, from: string): Promise<void> => {
241
+ async (
242
+ result: EmailSignInStepResult,
243
+ from: string,
244
+ request?: { requestId: string; requestSecret: string },
245
+ ): Promise<void> => {
229
246
  if (finishingRef.current) return;
247
+ if (isEmailSignInChooseAccount(result)) {
248
+ if (!request) return;
249
+ setError(null);
250
+ setNotice(null);
251
+ setStep({
252
+ name: 'choose',
253
+ identifier: from,
254
+ requestId: request.requestId,
255
+ requestSecret: request.requestSecret,
256
+ accounts: result.accounts,
257
+ });
258
+ return;
259
+ }
230
260
  if (isSecondFactorRequired(result)) {
231
261
  setError(null);
232
262
  setSecondFactorCode('');
@@ -301,7 +331,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
301
331
  requestSecret: request.requestSecret,
302
332
  code: typed.trim(),
303
333
  })
304
- .then((result) => finish(result, request.identifier))
334
+ .then((result) => finish(result, request.identifier, request))
305
335
  .catch((reason: unknown) => {
306
336
  setCode('');
307
337
  fail(reason);
@@ -309,6 +339,27 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
309
339
  .finally(() => setPending(false));
310
340
  };
311
341
 
342
+ const chooseAccount = (account: EmailSignInAccountOption) => {
343
+ if (step.name !== 'choose' || blocked) return;
344
+ const request = step;
345
+ const name = account.username ?? request.identifier;
346
+ setError(null);
347
+ setPending(true);
348
+ setChoosingId(account.id);
349
+ oxyServices.auth.email
350
+ .choose({
351
+ requestId: request.requestId,
352
+ requestSecret: request.requestSecret,
353
+ userId: account.id,
354
+ })
355
+ .then((result) => finish(result, name))
356
+ .catch(fail)
357
+ .finally(() => {
358
+ setPending(false);
359
+ setChoosingId(null);
360
+ });
361
+ };
362
+
312
363
  const submitPassword = () => {
313
364
  if (step.name !== 'password' || blocked) return;
314
365
  if (!password) {
@@ -369,7 +420,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
369
420
  try {
370
421
  const result = await oxyServices.auth.email.collect({ requestId, requestSecret });
371
422
  if (!('status' in result)) {
372
- await finishRef.current(result, requestIdentifier);
423
+ await finishRef.current(result, requestIdentifier, { requestId, requestSecret });
373
424
  return;
374
425
  }
375
426
  if (stopped) return;
@@ -550,6 +601,40 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
550
601
  );
551
602
  break;
552
603
  }
604
+ case 'choose': {
605
+ const request = step;
606
+ form = (
607
+ <OxyAuthScreen className={splits ? 'md:max-w-none' : undefined}>
608
+ <OxyAuthScreenHeader
609
+ title={t('signin.emailAccounts.title')}
610
+ description={t('signin.emailAccounts.description', {
611
+ identifier: request.identifier,
612
+ })}
613
+ />
614
+ <OxyEmailAccountChoice
615
+ accounts={request.accounts}
616
+ onChoose={chooseAccount}
617
+ pendingId={choosingId}
618
+ disabled={blocked}
619
+ />
620
+ {error ? (
621
+ <AccountFlowNotice type="error" testID="signin-choose-error">
622
+ {error}
623
+ </AccountFlowNotice>
624
+ ) : null}
625
+ {rateLimitNotice}
626
+ <View style={styles.links}>
627
+ <SubtleLink
628
+ label={t('signin.checkEmail.differentAccount')}
629
+ onPress={backToIdentifier}
630
+ disabled={pending}
631
+ testID="signin-different-account"
632
+ />
633
+ </View>
634
+ </OxyAuthScreen>
635
+ );
636
+ break;
637
+ }
553
638
  case 'password': {
554
639
  const request = step;
555
640
  form = (
@@ -85,6 +85,8 @@ export function describeSignInError(error: unknown, t: Translate): string {
85
85
  return t('signin.errors.originNotAllowed');
86
86
  case SIGN_IN_ERROR_CODES.usernameTaken:
87
87
  return t('signup.username.taken');
88
+ case SIGN_IN_ERROR_CODES.emailAccountsFull:
89
+ return t('signin.errors.emailAccountsFull');
88
90
  case EMAIL_VERIFICATION_ERROR_CODES.codeInvalid:
89
91
  return t('emailCode.errors.codeInvalid');
90
92
  case EMAIL_VERIFICATION_ERROR_CODES.tooManyAttempts: