@oxy.so/services 8.1.1 → 8.3.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 (112) hide show
  1. package/lib/commonjs/index.js +2 -0
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/OxyProvider.js +5 -1
  4. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  5. package/lib/commonjs/ui/components/RequireOxyAuth.js +9 -1
  6. package/lib/commonjs/ui/components/RequireOxyAuth.js.map +1 -1
  7. package/lib/commonjs/ui/components/signIn/OxyAuthenticatorPanel.js +3 -7
  8. package/lib/commonjs/ui/components/signIn/OxyAuthenticatorPanel.js.map +1 -1
  9. package/lib/commonjs/ui/components/signIn/OxySignInPanel.js +34 -27
  10. package/lib/commonjs/ui/components/signIn/OxySignInPanel.js.map +1 -1
  11. package/lib/commonjs/ui/components/signIn/ReauthStep.js +27 -19
  12. package/lib/commonjs/ui/components/signIn/ReauthStep.js.map +1 -1
  13. package/lib/commonjs/ui/components/signIn/accountFlowParts.js +54 -22
  14. package/lib/commonjs/ui/components/signIn/accountFlowParts.js.map +1 -1
  15. package/lib/commonjs/ui/context/OxyContext.js +25 -4
  16. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/accountQueryPersistence.js +157 -0
  18. package/lib/commonjs/ui/hooks/accountQueryPersistence.js.map +1 -0
  19. package/lib/commonjs/ui/hooks/queryClient.js +17 -9
  20. package/lib/commonjs/ui/hooks/queryClient.js.map +1 -1
  21. package/lib/commonjs/ui/index.js.map +1 -1
  22. package/lib/commonjs/ui/utils/buildId.js +25 -0
  23. package/lib/commonjs/ui/utils/buildId.js.map +1 -0
  24. package/lib/module/index.js +3 -0
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/ui/components/OxyProvider.js +5 -1
  27. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  28. package/lib/module/ui/components/RequireOxyAuth.js +9 -1
  29. package/lib/module/ui/components/RequireOxyAuth.js.map +1 -1
  30. package/lib/module/ui/components/signIn/OxyAuthenticatorPanel.js +4 -8
  31. package/lib/module/ui/components/signIn/OxyAuthenticatorPanel.js.map +1 -1
  32. package/lib/module/ui/components/signIn/OxySignInPanel.js +36 -29
  33. package/lib/module/ui/components/signIn/OxySignInPanel.js.map +1 -1
  34. package/lib/module/ui/components/signIn/ReauthStep.js +29 -21
  35. package/lib/module/ui/components/signIn/ReauthStep.js.map +1 -1
  36. package/lib/module/ui/components/signIn/accountFlowParts.js +51 -19
  37. package/lib/module/ui/components/signIn/accountFlowParts.js.map +1 -1
  38. package/lib/module/ui/context/OxyContext.js +27 -6
  39. package/lib/module/ui/context/OxyContext.js.map +1 -1
  40. package/lib/module/ui/hooks/accountQueryPersistence.js +153 -0
  41. package/lib/module/ui/hooks/accountQueryPersistence.js.map +1 -0
  42. package/lib/module/ui/hooks/queryClient.js +18 -10
  43. package/lib/module/ui/hooks/queryClient.js.map +1 -1
  44. package/lib/module/ui/index.js +3 -0
  45. package/lib/module/ui/index.js.map +1 -1
  46. package/lib/module/ui/utils/buildId.js +21 -0
  47. package/lib/module/ui/utils/buildId.js.map +1 -0
  48. package/lib/typescript/commonjs/index.d.ts +1 -0
  49. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/ui/components/RequireOxyAuth.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/ui/components/signIn/OxyAuthenticatorPanel.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts +6 -3
  54. package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/ui/components/signIn/ReauthStep.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts +29 -9
  57. package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +9 -0
  60. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/ui/hooks/accountQueryPersistence.d.ts +54 -0
  62. package/lib/typescript/commonjs/ui/hooks/accountQueryPersistence.d.ts.map +1 -0
  63. package/lib/typescript/commonjs/ui/hooks/queryClient.d.ts +8 -2
  64. package/lib/typescript/commonjs/ui/hooks/queryClient.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/icons/Ionicons.d.ts +1 -1
  66. package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  67. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  68. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/types/navigation.d.ts +9 -0
  70. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/utils/buildId.d.ts +16 -0
  72. package/lib/typescript/commonjs/ui/utils/buildId.d.ts.map +1 -0
  73. package/lib/typescript/module/index.d.ts +1 -0
  74. package/lib/typescript/module/index.d.ts.map +1 -1
  75. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  76. package/lib/typescript/module/ui/components/RequireOxyAuth.d.ts.map +1 -1
  77. package/lib/typescript/module/ui/components/signIn/OxyAuthenticatorPanel.d.ts.map +1 -1
  78. package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts +6 -3
  79. package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
  80. package/lib/typescript/module/ui/components/signIn/ReauthStep.d.ts.map +1 -1
  81. package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts +29 -9
  82. package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
  83. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  84. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +9 -0
  85. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  86. package/lib/typescript/module/ui/hooks/accountQueryPersistence.d.ts +54 -0
  87. package/lib/typescript/module/ui/hooks/accountQueryPersistence.d.ts.map +1 -0
  88. package/lib/typescript/module/ui/hooks/queryClient.d.ts +8 -2
  89. package/lib/typescript/module/ui/hooks/queryClient.d.ts.map +1 -1
  90. package/lib/typescript/module/ui/icons/Ionicons.d.ts +1 -1
  91. package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  92. package/lib/typescript/module/ui/index.d.ts +1 -0
  93. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/types/navigation.d.ts +9 -0
  95. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  96. package/lib/typescript/module/ui/utils/buildId.d.ts +16 -0
  97. package/lib/typescript/module/ui/utils/buildId.d.ts.map +1 -0
  98. package/package.json +7 -7
  99. package/src/index.ts +2 -0
  100. package/src/ui/components/OxyProvider.tsx +5 -1
  101. package/src/ui/components/RequireOxyAuth.tsx +7 -2
  102. package/src/ui/components/signIn/OxyAuthenticatorPanel.tsx +4 -8
  103. package/src/ui/components/signIn/OxySignInPanel.tsx +41 -27
  104. package/src/ui/components/signIn/ReauthStep.tsx +33 -21
  105. package/src/ui/components/signIn/accountFlowParts.tsx +64 -22
  106. package/src/ui/context/OxyContext.tsx +35 -5
  107. package/src/ui/context/oxyContextTypes.ts +9 -0
  108. package/src/ui/hooks/accountQueryPersistence.ts +201 -0
  109. package/src/ui/hooks/queryClient.ts +17 -8
  110. package/src/ui/index.ts +2 -0
  111. package/src/ui/types/navigation.ts +9 -0
  112. package/src/ui/utils/buildId.ts +18 -0
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../../../src/ui/types/navigation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../../../src/ui/types/navigation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The identity of the JS bundle that is running, used as the buster for every
3
+ * persisted query cache the SDK owns.
4
+ *
5
+ * A persisted cache is restored and served before any refetch (offline-first),
6
+ * so data written by an older build reaches components in whatever shape that
7
+ * build used. Busting on the build means no app ever has to remember to bump a
8
+ * version when a query's shape changes: a new build simply starts from the
9
+ * network. Inbox shipped exactly that crash when a hand-kept buster was missed.
10
+ *
11
+ * `@oxy.so/app-preset`'s Babel config replaces `process.env.OXY_BUILD_ID` with
12
+ * the commit at bundle time, node_modules included. An app that does not use
13
+ * the preset gets a constant, which behaves like a buster that is never bumped.
14
+ */
15
+ export declare function getOxyBuildId(): string;
16
+ //# sourceMappingURL=buildId.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildId.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/buildId.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAGtC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/services",
3
- "version": "8.1.1",
3
+ "version": "8.3.0",
4
4
  "description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -155,7 +155,7 @@
155
155
  },
156
156
  "dependencies": {
157
157
  "@expo/config-plugins": "57.0.9",
158
- "@oxy.so/contracts": "^4.0.0",
158
+ "@oxy.so/contracts": "^4.1.0",
159
159
  "color": "^4.2.3"
160
160
  },
161
161
  "devDependencies": {
@@ -163,7 +163,7 @@
163
163
  "@commitlint/cli": "^21.2.1",
164
164
  "@commitlint/config-conventional": "^21.2.0",
165
165
  "@expo/vector-icons": "^15.0.3",
166
- "@oxy.so/core": "3.0.2",
166
+ "@oxy.so/core": "3.1.0",
167
167
  "@react-native-async-storage/async-storage": "^2.0.0",
168
168
  "@react-native-community/netinfo": "^12.0.1",
169
169
  "@react-native/babel-preset": "^0.86.2",
@@ -178,7 +178,7 @@
178
178
  "@types/jest": "^30.0.0",
179
179
  "@types/node": "^20.19.43",
180
180
  "@types/react": "*",
181
- "@oxy.so/bloom": "^4.26.0",
181
+ "@oxy.so/bloom": "^4.31.0",
182
182
  "copyfiles": "^2.4.1",
183
183
  "expo-constants": "~57.0.5",
184
184
  "expo-file-system": "~57.0.1",
@@ -211,11 +211,11 @@
211
211
  "typescript": "^5.9.2",
212
212
  "zustand": "^5.0.14"
213
213
  },
214
- "//@oxy.so/bloom": "RE-RAISE THIS FLOOR ON EVERY BREAKING BLOOM RELEASE, in the same commit as the root package.json catalog entry and the code that requires it. The breaking position is the MAJOR, so `^3.0.1` means `>=3.0.1 <4.0.0`: 3.x minors are admitted on semver's additive promise, and Bloom 4.0.0 is refused until someone raises this deliberately. The CAP is the part that is load-bearing. An earlier revision of this key recorded the opposite trade -- an uncapped `>=` floor, accepted so that intentionally-breaking Bloom 0.x minors would not be blocked -- and that trade is closed by evidence rather than by preference: an unbounded range is exactly what let `bun install` pair this package with Bloom 1.0.0 and emit no warning at all, and 1.0.0 broke it four ways (a deleted provider, a renamed confirm option, a renamed Chip fill, and two REMOVED SUBPATHS that fail at bundle time, before any typecheck runs). A floor is a claim about what was measured; a cap is the claim that nothing beyond it was. RAISED TO 3.0.1 from `^2.1.0`. Bloom 3.0.0 breaks exactly two things -- `PageHeader` defaults to `presentation=\"floating\"` and stops hiding the title under `transparent`, and `Field disabled` now reaches the control that reads the field context -- and THIS PACKAGE IMPORTS NEITHER: it renders no `PageHeader` and no `Field`, and 3.0 removes no subpath (unlike 2.0.0, which is why that floor cost code changes and this one does not). Its six `transparent` occurrences are CSS colour values, not the PageHeader prop. The cap had to move rather than widen to a union, because leaving `^2.1.0` in place is what made `bun install` resolve a SECOND, nested Bloom 2.12.4 under this package for any app on Bloom 3 -- two copies of a library whose 3.0 contracts (`control-surface`, `TopEdgeProvider`, `ScrollOffsetProvider`) are React contexts, so a provider from one copy cannot be read by a consumer from the other. Mention's `oxyRuntimeCompatibility` test is what caught it; nothing in a typecheck can. RAISED AGAIN TO 3.2.0 from `^3.0.1`, for the same class of reason the 2.x note records, only in the other direction: 3.2.0 ADDS the per-glyph icon subpaths (`@oxy.so/bloom/icons/RiAtLine`) that this package's screens now import instead of the `@oxy.so/bloom/icons` barrel, and a missing subpath fails at bundle time, in the consumer's Metro, long after this package has typechecked and shipped. `^3.0.1` would have admitted Bloom 3.0.1 and 3.1.0, neither of which has those subpaths, so the floor is the only thing standing between a consumer and a red build. Nothing else about 3.1.0 or 3.2.0 is required here; the cap stays at the major. RAISED TO ^4.2.0: FollowButton now delegates its visuals to Bloom media-header FollowButton with loading, textStyle and large size; the new shared UI contracts require one Bloom4 context across Services and its consumer.",
214
+ "//@oxy.so/bloom": "RE-RAISE THIS FLOOR ON EVERY BREAKING BLOOM RELEASE, in the same commit as the root package.json catalog entry and the code that requires it. The breaking position is the MAJOR, so `^3.0.1` means `>=3.0.1 <4.0.0`: 3.x minors are admitted on semver's additive promise, and Bloom 4.0.0 is refused until someone raises this deliberately. The CAP is the part that is load-bearing. An earlier revision of this key recorded the opposite trade -- an uncapped `>=` floor, accepted so that intentionally-breaking Bloom 0.x minors would not be blocked -- and that trade is closed by evidence rather than by preference: an unbounded range is exactly what let `bun install` pair this package with Bloom 1.0.0 and emit no warning at all, and 1.0.0 broke it four ways (a deleted provider, a renamed confirm option, a renamed Chip fill, and two REMOVED SUBPATHS that fail at bundle time, before any typecheck runs). A floor is a claim about what was measured; a cap is the claim that nothing beyond it was. RAISED TO 3.0.1 from `^2.1.0`. Bloom 3.0.0 breaks exactly two things -- `PageHeader` defaults to `presentation=\"floating\"` and stops hiding the title under `transparent`, and `Field disabled` now reaches the control that reads the field context -- and THIS PACKAGE IMPORTS NEITHER: it renders no `PageHeader` and no `Field`, and 3.0 removes no subpath (unlike 2.0.0, which is why that floor cost code changes and this one does not). Its six `transparent` occurrences are CSS colour values, not the PageHeader prop. The cap had to move rather than widen to a union, because leaving `^2.1.0` in place is what made `bun install` resolve a SECOND, nested Bloom 2.12.4 under this package for any app on Bloom 3 -- two copies of a library whose 3.0 contracts (`control-surface`, `TopEdgeProvider`, `ScrollOffsetProvider`) are React contexts, so a provider from one copy cannot be read by a consumer from the other. Mention's `oxyRuntimeCompatibility` test is what caught it; nothing in a typecheck can. RAISED AGAIN TO 3.2.0 from `^3.0.1`, for the same class of reason the 2.x note records, only in the other direction: 3.2.0 ADDS the per-glyph icon subpaths (`@oxy.so/bloom/icons/RiAtLine`) that this package's screens now import instead of the `@oxy.so/bloom/icons` barrel, and a missing subpath fails at bundle time, in the consumer's Metro, long after this package has typechecked and shipped. `^3.0.1` would have admitted Bloom 3.0.1 and 3.1.0, neither of which has those subpaths, so the floor is the only thing standing between a consumer and a red build. Nothing else about 3.1.0 or 3.2.0 is required here; the cap stays at the major. RAISED TO ^4.2.0: FollowButton now delegates its visuals to Bloom media-header FollowButton with loading, textStyle and large size; the new shared UI contracts require one Bloom4 context across Services and its consumer. RAISED TO ^4.31.0: the sign-in and account screens type one-time codes into `InputOtp` with `type=\"alphanumeric\"` (the long sign-in code and backup codes), which 4.31.0 adds; older 4.x drops the letters.",
215
215
  "peerDependencies": {
216
216
  "@expo/vector-icons": "^15.0.3",
217
- "@oxy.so/bloom": "^4.26.0",
218
- "@oxy.so/core": "^3.0.0",
217
+ "@oxy.so/bloom": "^4.31.0",
218
+ "@oxy.so/core": "^3.1.0",
219
219
  "@react-native-async-storage/async-storage": "^2.0.0",
220
220
  "@react-native-community/netinfo": ">=11.4.1",
221
221
  "@tanstack/query-async-storage-persister": "^5.101",
package/src/index.ts CHANGED
@@ -306,6 +306,8 @@ export type {
306
306
  // wall before the device-first cold boot resolves. Opens the ONE account dialog.
307
307
  export { RequireOxyAuth } from './ui/components/RequireOxyAuth';
308
308
  export type { RequireOxyAuthProps, RequireOxyAuthPrompt } from './ui/components/RequireOxyAuth';
309
+ // `OxyProvider`'s `accountQueries`: the app's per-account offline cache.
310
+ export type { AccountQueriesConfig } from './ui/hooks/accountQueryPersistence';
309
311
 
310
312
  export { default as FollowButton } from './ui/components/FollowButton';
311
313
  export type { FollowButtonProps, SingleFollowButtonProps, MultiFollowButtonProps } from './ui/components/FollowButton';
@@ -88,6 +88,7 @@ const OxyProvider: FC<OxyProviderProps> = ({
88
88
  sessionMode = 'account',
89
89
  webAuthMode = 'popup',
90
90
  queryClient: providedQueryClient,
91
+ accountQueries,
91
92
  requireAuth = 'off',
92
93
  backgroundSession = false,
93
94
  language,
@@ -102,6 +103,8 @@ const OxyProvider: FC<OxyProviderProps> = ({
102
103
  const queryClientRef = useRef<ReturnType<typeof createQueryClient> | null>(null);
103
104
  const persistenceUnsubRef = useRef<(() => void) | null>(null);
104
105
  const ownsQueryClientRef = useRef(providedQueryClient === undefined);
106
+ // `accountQueries` is read once, at mount, like the client itself.
107
+ const accountMutationKeysRef = useRef(accountQueries?.mutationKeys);
105
108
  const [platformStorage, setPlatformStorage] = useState<StorageInterface | null>(null);
106
109
 
107
110
  // If the consumer supplied their own QueryClient we use it as-is and skip
@@ -135,7 +138,7 @@ const OxyProvider: FC<OxyProviderProps> = ({
135
138
  setPlatformStorage(storage);
136
139
  const client = queryClientRef.current;
137
140
  if (!client || !ownsQueryClientRef.current) return;
138
- const persistence = attachQueryPersistence(client, storage);
141
+ const persistence = attachQueryPersistence(client, storage, accountMutationKeysRef.current);
139
142
  persistenceUnsubRef.current = persistence.unsubscribe;
140
143
  await persistence.restored;
141
144
  };
@@ -240,6 +243,7 @@ const OxyProvider: FC<OxyProviderProps> = ({
240
243
  webAuthMode={webAuthMode}
241
244
  backgroundSession={backgroundSession}
242
245
  platformStorage={platformStorage}
246
+ accountQueries={accountQueries}
243
247
  onAuthStateChange={onAuthStateChange as OxyRuntimeProviderProps['onAuthStateChange']}
244
248
  >
245
249
  {productAnalytics ? <ProductAnalyticsObserver analytics={productAnalytics} /> : null}
@@ -75,7 +75,7 @@ export const RequireOxyAuth: React.FC<RequireOxyAuthProps> = ({
75
75
  bannerMessage,
76
76
  bannerActionLabel,
77
77
  }) => {
78
- const { canUsePrivateApi, isPrivateApiPending, openAccountDialog } = useOxy();
78
+ const { canUsePrivateApi, isPrivateApiPending, isAccountCacheReady, openAccountDialog } = useOxy();
79
79
 
80
80
  // Public app: render straight through. Cheap enough to short-circuit before
81
81
  // touching any gate UI.
@@ -83,8 +83,13 @@ export const RequireOxyAuth: React.FC<RequireOxyAuthProps> = ({
83
83
  return <>{children}</>;
84
84
  }
85
85
 
86
- // Signed in (and token ready): render the protected subtree in every mode.
86
+ // Signed in (and token ready): render the protected subtree in every mode,
87
+ // once the account's offline cache (`accountQueries`) has restored. Before
88
+ // that it would render with no rows, or with the previous account's.
87
89
  if (canUsePrivateApi) {
90
+ if (!isAccountCacheReady) {
91
+ return loadingFallback ? <>{loadingFallback}</> : <NeutralLoading />;
92
+ }
88
93
  return <>{children}</>;
89
94
  }
90
95
 
@@ -30,8 +30,8 @@ import { OxyAuthLoading, OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScr
30
30
  import { ReauthStep } from './ReauthStep';
31
31
  import {
32
32
  AccountFlowAction,
33
+ AccountFlowCodeField,
33
34
  AccountFlowErrorLine,
34
- AccountFlowField,
35
35
  AccountFlowNote,
36
36
  describeSignInError,
37
37
  } from './accountFlowParts';
@@ -161,16 +161,12 @@ export const OxyAuthenticatorPanel: React.FC<OxyAuthenticatorPanelProps> = ({ on
161
161
  {enrollment.secret.replace(/(.{4})/g, '$1 ').trim()}
162
162
  </Text>
163
163
  </View>
164
- <AccountFlowField
164
+ <AccountFlowCodeField
165
165
  label={t('signInSecurity.totp.codeLabel')}
166
166
  value={code}
167
- onChange={(value) => setCode(value.replace(/\D/g, '').slice(0, TOTP_DIGITS))}
168
- onSubmit={() => undefined}
167
+ onChange={setCode}
169
168
  error={null}
170
- placeholder="000000"
171
- autoComplete="one-time-code"
172
- keyboardType="number-pad"
173
- maxLength={TOTP_DIGITS}
169
+ length={TOTP_DIGITS}
174
170
  testID="totp-enroll-code"
175
171
  />
176
172
  </ReauthStep>
@@ -10,9 +10,12 @@
10
10
  * photo carousel, in the right column (the account dialog grows to
11
11
  * that card); below `md` it is "Continue with Oxy" at the top. On
12
12
  * native it is "Continue with Oxy" ("Get Commons" without Commons).
13
- * check-email one email carries a code and a link. The code is typed here (6
14
- * digits, or the 10-character long code); meanwhile the screen asks
15
- * whether the link was opened in this browser, and signs in when it was.
13
+ * check-email one email carries a code and a link. The code is typed here: 6
14
+ * digits, or — "Does your code have letters?" — the 10-character
15
+ * long code an account past its daily guessing ceiling is sent
16
+ * (the start response does not say which, by design); meanwhile
17
+ * the screen asks whether the link was opened in this browser,
18
+ * and signs in when it was.
16
19
  * password the alternative for an account that has one.
17
20
  * second-factor the authenticator's code, or a backup code, when the account has one.
18
21
  *
@@ -32,6 +35,7 @@ import { toast } from '@oxy.so/bloom/toast';
32
35
  import { Text } from '@oxy.so/bloom/typography';
33
36
  import type { SwitcherContextRow } from '@oxy.so/core/session';
34
37
  import {
38
+ EMAIL_CODE_LENGTH,
35
39
  EMAIL_SIGNIN_LONG_CODE_LENGTH,
36
40
  SIGN_IN_ERROR_CODES,
37
41
  TOTP_DIGITS,
@@ -54,11 +58,12 @@ import { OxyAccountPicker } from './OxyAccountPicker';
54
58
  import { OxyAuthScreen, OxyAuthScreenHeader, OxyAuthSplit, OxyAuthTerms } from './OxyAuthScreen';
55
59
  import {
56
60
  AccountFlowAction,
61
+ AccountFlowCodeField,
57
62
  AccountFlowField,
58
63
  AccountFlowNote,
64
+ BACKUP_CODE_LENGTH,
59
65
  describeSignInError,
60
66
  errorCode,
61
- formatSignInCodeInput,
62
67
  isCompleteSignInCode,
63
68
  isRateLimited,
64
69
  retryAfterSeconds,
@@ -79,6 +84,8 @@ interface SavedSignInFlow {
79
84
  identifier: string;
80
85
  showForm: boolean;
81
86
  useBackupCode: boolean;
87
+ /** The code field takes the 10-character long code ("Does your code have letters?"). */
88
+ longCode: boolean;
82
89
  /** When "Send a new email" may be pressed again (ms). */
83
90
  resendUntil: number;
84
91
  }
@@ -139,6 +146,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
139
146
  const [password, setPassword] = useState('');
140
147
  const [secondFactorCode, setSecondFactorCode] = useState('');
141
148
  const [useBackupCode, setUseBackupCode] = useState(saved?.useBackupCode ?? false);
149
+ const [longCode, setLongCode] = useState(saved?.longCode ?? false);
142
150
  const [pending, setPending] = useState(false);
143
151
  const [error, setError] = useState<string | null>(null);
144
152
  const [notice, setNotice] = useState<string | null>(null);
@@ -153,9 +161,10 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
153
161
  identifier,
154
162
  showForm,
155
163
  useBackupCode,
164
+ longCode,
156
165
  resendUntil: Date.now() + resendSeconds * 1000,
157
166
  } satisfies SavedSignInFlow);
158
- }, [flowOwner, step, identifier, showForm, useBackupCode, resendSeconds]);
167
+ }, [flowOwner, step, identifier, showForm, useBackupCode, longCode, resendSeconds]);
159
168
  const blocked = pending || rateLimitSeconds > 0;
160
169
 
161
170
  // The countdown after a 429: one tick a second until the person may retry.
@@ -244,7 +253,9 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
244
253
  }
245
254
  setCode('');
246
255
  setResendSeconds(EMAIL_RESEND_COOLDOWN_SECONDS);
256
+ // A resend keeps the field as the person set it; a new request starts at 6 digits.
247
257
  if (step.name === 'check-email') setNotice(t('signin.checkEmail.resent'));
258
+ else setLongCode(false);
248
259
  setStep({ name: 'check-email', identifier: name, requestId: started.requestId, requestSecret: started.requestSecret });
249
260
  })
250
261
  .catch(fail)
@@ -362,6 +373,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
362
373
  setError(null);
363
374
  setNotice(null);
364
375
  setCode('');
376
+ setLongCode(false);
365
377
  setPassword('');
366
378
  setStep({ name: 'start' });
367
379
  };
@@ -443,24 +455,34 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
443
455
  title={t('signin.checkEmail.title')}
444
456
  description={t('signin.checkEmail.description', { identifier: request.identifier })}
445
457
  />
446
- <AccountFlowField
458
+ <AccountFlowCodeField
459
+ key={longCode ? 'long' : 'digits'}
447
460
  label={t('signin.checkEmail.codeLabel')}
448
461
  value={code}
449
462
  onChange={(value) => {
450
- const typed = formatSignInCodeInput(value);
451
- setCode(typed);
463
+ setCode(value);
452
464
  clearError();
453
- if (isCompleteSignInCode(typed)) submitCode(typed);
454
465
  }}
455
- onSubmit={() => submitCode(code)}
466
+ onComplete={submitCode}
456
467
  error={shownError}
457
468
  disabled={blocked}
458
- placeholder="000000"
459
- autoComplete="one-time-code"
460
- maxLength={EMAIL_SIGNIN_LONG_CODE_LENGTH + 1}
469
+ length={longCode ? EMAIL_SIGNIN_LONG_CODE_LENGTH : EMAIL_CODE_LENGTH}
470
+ type={longCode ? 'alphanumeric' : 'numeric'}
471
+ groupEvery={longCode ? EMAIL_SIGNIN_LONG_CODE_LENGTH / 2 : undefined}
461
472
  hint={t('signin.checkEmail.codeHint')}
462
473
  testID="signin-code"
463
474
  />
475
+ <SubtleLink
476
+ label={longCode ? t('signin.checkEmail.codeDigitsOnly') : t('signin.checkEmail.codeHasLetters')}
477
+ theme={theme}
478
+ onPress={() => {
479
+ setLongCode(!longCode);
480
+ setCode('');
481
+ setError(null);
482
+ }}
483
+ disabled={pending}
484
+ testID="signin-code-letters"
485
+ />
464
486
  {notice ? <AccountFlowNote testID="signin-notice">{notice}</AccountFlowNote> : null}
465
487
  <AccountFlowAction
466
488
  label={t('signin.actions.continue')}
@@ -553,28 +575,20 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
553
575
  title={t('signin.secondFactor.title')}
554
576
  description={useBackupCode ? t('signin.secondFactor.backupDescription') : t('signin.secondFactor.description')}
555
577
  />
556
- <AccountFlowField
578
+ <AccountFlowCodeField
557
579
  key={useBackupCode ? 'backup' : 'totp'}
558
580
  label={useBackupCode ? t('signin.secondFactor.backupLabel') : t('signin.secondFactor.label')}
559
581
  value={secondFactorCode}
560
582
  onChange={(value) => {
561
- if (useBackupCode) {
562
- setSecondFactorCode(value);
563
- clearError();
564
- return;
565
- }
566
- const digits = value.replace(/\D/g, '').slice(0, TOTP_DIGITS);
567
- setSecondFactorCode(digits);
583
+ setSecondFactorCode(value);
568
584
  clearError();
569
- if (digits.length === TOTP_DIGITS) submitSecondFactor(digits);
570
585
  }}
571
- onSubmit={() => submitSecondFactor(secondFactorCode)}
586
+ onComplete={submitSecondFactor}
572
587
  error={shownError}
573
588
  disabled={blocked}
574
- placeholder={useBackupCode ? 'xxxxx-xxxxx' : '000000'}
575
- autoComplete="one-time-code"
576
- keyboardType={useBackupCode ? 'default' : 'number-pad'}
577
- maxLength={useBackupCode ? 11 : TOTP_DIGITS}
589
+ length={useBackupCode ? BACKUP_CODE_LENGTH : TOTP_DIGITS}
590
+ type={useBackupCode ? 'alphanumeric' : 'numeric'}
591
+ groupEvery={useBackupCode ? BACKUP_CODE_LENGTH / 2 : undefined}
578
592
  testID="signin-second-factor"
579
593
  />
580
594
  <AccountFlowAction
@@ -9,16 +9,18 @@
9
9
  import type React from 'react';
10
10
  import { useState } from 'react';
11
11
  import { useTheme } from '@oxy.so/bloom/theme';
12
- import { EMAIL_CODE_LENGTH, type ReauthAction, type ReauthProof } from '@oxy.so/contracts';
12
+ import { EMAIL_CODE_LENGTH, TOTP_DIGITS, type ReauthAction, type ReauthProof } from '@oxy.so/contracts';
13
13
  import { useOxy } from '../../context/OxyContext';
14
14
  import { useI18n } from '../../hooks/useI18n';
15
15
  import { SubtleLink } from '../authChooser/primitives';
16
16
  import { OxyAuthScreen, OxyAuthScreenHeader } from './OxyAuthScreen';
17
17
  import {
18
18
  AccountFlowAction,
19
+ AccountFlowCodeField,
19
20
  AccountFlowErrorLine,
20
21
  AccountFlowField,
21
22
  AccountFlowNote,
23
+ BACKUP_CODE_LENGTH,
22
24
  describeSignInError,
23
25
  } from './accountFlowParts';
24
26
 
@@ -64,6 +66,7 @@ export const ReauthStep: React.FC<ReauthStepProps> = ({
64
66
  const [emailCode, setEmailCode] = useState('');
65
67
  const [password, setPassword] = useState('');
66
68
  const [totpCode, setTotpCode] = useState('');
69
+ const [useBackupCode, setUseBackupCode] = useState(false);
67
70
  const [pending, setPending] = useState(false);
68
71
  const [error, setError] = useState<string | null>(null);
69
72
 
@@ -138,17 +141,13 @@ export const ReauthStep: React.FC<ReauthStepProps> = ({
138
141
  ) : verificationId ? (
139
142
  <>
140
143
  <AccountFlowNote>{t('reauth.codeSent')}</AccountFlowNote>
141
- <AccountFlowField
144
+ <AccountFlowCodeField
142
145
  label={t('reauth.codeLabel')}
143
146
  value={emailCode}
144
- onChange={(value) => setEmailCode(value.replace(/\D/g, '').slice(0, EMAIL_CODE_LENGTH))}
145
- onSubmit={submit}
147
+ onChange={setEmailCode}
146
148
  error={null}
147
149
  disabled={pending}
148
- placeholder="000000"
149
- autoComplete="one-time-code"
150
- keyboardType="number-pad"
151
- maxLength={EMAIL_CODE_LENGTH}
150
+ length={EMAIL_CODE_LENGTH}
152
151
  testID="reauth-code"
153
152
  />
154
153
  </>
@@ -156,19 +155,32 @@ export const ReauthStep: React.FC<ReauthStepProps> = ({
156
155
  <AccountFlowNote>{t('reauth.emailDescription')}</AccountFlowNote>
157
156
  )}
158
157
  {totpEnabled && !needsCode ? (
159
- <AccountFlowField
160
- label={t('reauth.totpLabel')}
161
- value={totpCode}
162
- onChange={setTotpCode}
163
- onSubmit={submit}
164
- error={null}
165
- disabled={pending}
166
- placeholder="000000"
167
- autoComplete="one-time-code"
168
- autoFocus={false}
169
- maxLength={11}
170
- testID="reauth-totp"
171
- />
158
+ <>
159
+ <AccountFlowCodeField
160
+ key={useBackupCode ? 'backup' : 'totp'}
161
+ label={useBackupCode ? t('signin.secondFactor.backupLabel') : t('signin.secondFactor.label')}
162
+ value={totpCode}
163
+ onChange={setTotpCode}
164
+ error={null}
165
+ disabled={pending}
166
+ length={useBackupCode ? BACKUP_CODE_LENGTH : TOTP_DIGITS}
167
+ type={useBackupCode ? 'alphanumeric' : 'numeric'}
168
+ groupEvery={useBackupCode ? BACKUP_CODE_LENGTH / 2 : undefined}
169
+ autoFocus={false}
170
+ testID="reauth-totp"
171
+ />
172
+ <SubtleLink
173
+ label={useBackupCode ? t('signin.secondFactor.useAuthenticator') : t('signin.secondFactor.useBackup')}
174
+ theme={theme}
175
+ onPress={() => {
176
+ setUseBackupCode(!useBackupCode);
177
+ setTotpCode('');
178
+ setError(null);
179
+ }}
180
+ disabled={pending}
181
+ testID="reauth-toggle-backup"
182
+ />
183
+ </>
172
184
  ) : null}
173
185
  {error ? <AccountFlowErrorLine message={error} /> : null}
174
186
  <AccountFlowAction
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * The pieces every Oxy sign-in and account screen shares: the error vocabulary,
3
- * one labelled field, the one primary action, a note, and the email-code step.
3
+ * one labelled field, one one-time-code field, the one primary action, a note,
4
+ * and the email-code step.
4
5
  * Each screen is built from the same shell as `OxySignInPanel`
5
6
  * (`OxyAuthScreen`), so they read as one product in the account dialog, in an
6
7
  * app's settings and on auth.oxy.so.
@@ -10,6 +11,8 @@ import type React from 'react';
10
11
  import { useState } from 'react';
11
12
  import { StyleSheet, View } from 'react-native';
12
13
  import { Button } from '@oxy.so/bloom/button';
14
+ import { Field } from '@oxy.so/bloom/field';
15
+ import { InputOtp } from '@oxy.so/bloom/input-otp';
13
16
  import { useTheme } from '@oxy.so/bloom/theme';
14
17
  import { TextField, TextFieldHint, TextFieldInput, TextFieldLabel } from '@oxy.so/bloom/text-field';
15
18
  import { Text } from '@oxy.so/bloom/typography';
@@ -105,9 +108,7 @@ const LONG_CODE = new RegExp(`^[${EMAIL_SIGNIN_LONG_CODE_ALPHABET}]{${EMAIL_SIGN
105
108
 
106
109
  /**
107
110
  * Whether a typed sign-in code is complete: 6 digits, or the 10-character long
108
- * code (`XXXXX-XXXXX`, any case, with or without its dash). Six digits count
109
- * only when typed without a separator: the long code is shown with its dash
110
- * after the fifth character, so a long code being typed never reads as 6 digits.
111
+ * code (`XXXXX-XXXXX`, any case, with or without its dash) in its own alphabet.
111
112
  */
112
113
  export function isCompleteSignInCode(typed: string): boolean {
113
114
  const trimmed = typed.trim();
@@ -115,10 +116,8 @@ export function isCompleteSignInCode(typed: string): boolean {
115
116
  return LONG_CODE.test(normalizeEmailSignInCode(trimmed));
116
117
  }
117
118
 
118
- /** A typed sign-in code as it shows in its field: upper-case, the long code's dash kept. */
119
- export function formatSignInCodeInput(typed: string): string {
120
- return typed.toUpperCase().replace(/[^0-9A-Z-\s]/g, '').slice(0, EMAIL_SIGNIN_LONG_CODE_LENGTH + 1);
121
- }
119
+ /** Characters in a backup code (`xxxxx-xxxxx`, its dash not counted). */
120
+ export const BACKUP_CODE_LENGTH = 10;
122
121
 
123
122
  /** One labelled input, the way the sign-in screen draws its fields. */
124
123
  export const AccountFlowField: React.FC<{
@@ -129,11 +128,10 @@ export const AccountFlowField: React.FC<{
129
128
  error: string | null;
130
129
  disabled?: boolean;
131
130
  placeholder?: string;
132
- autoComplete?: 'username' | 'email' | 'one-time-code' | 'off' | 'current-password' | 'new-password';
133
- keyboardType?: 'default' | 'email-address' | 'number-pad';
131
+ autoComplete?: 'username' | 'email' | 'off' | 'current-password' | 'new-password';
132
+ keyboardType?: 'default' | 'email-address';
134
133
  secureTextEntry?: boolean;
135
134
  autoFocus?: boolean;
136
- maxLength?: number;
137
135
  hint?: string;
138
136
  testID: string;
139
137
  }> = ({
@@ -148,7 +146,6 @@ export const AccountFlowField: React.FC<{
148
146
  keyboardType,
149
147
  secureTextEntry,
150
148
  autoFocus = true,
151
- maxLength,
152
149
  hint,
153
150
  testID,
154
151
  }) => (
@@ -164,7 +161,6 @@ export const AccountFlowField: React.FC<{
164
161
  autoComplete={autoComplete}
165
162
  keyboardType={keyboardType}
166
163
  secureTextEntry={secureTextEntry}
167
- maxLength={maxLength}
168
164
  autoCapitalize="none"
169
165
  autoCorrect={false}
170
166
  autoFocus={autoFocus}
@@ -177,6 +173,57 @@ export const AccountFlowField: React.FC<{
177
173
  </View>
178
174
  );
179
175
 
176
+ /**
177
+ * A one-time code: Bloom's `InputOtp`, one box per character, named by the
178
+ * field's label (the boxes are the field's `multiple` case). `numeric` codes
179
+ * are the email's 6 digits and the authenticator's; `alphanumeric` ones are
180
+ * the long sign-in code and a backup code, both `XXXXX-XXXXX` — upper-cased,
181
+ * the dash dropped, so a pasted code fills every box.
182
+ */
183
+ export const AccountFlowCodeField: React.FC<{
184
+ label: string;
185
+ value: string;
186
+ onChange: (value: string) => void;
187
+ /** Every box is filled. */
188
+ onComplete?: (value: string) => void;
189
+ error: string | null;
190
+ disabled?: boolean;
191
+ length: number;
192
+ type?: 'numeric' | 'alphanumeric';
193
+ groupEvery?: number;
194
+ autoFocus?: boolean;
195
+ hint?: string;
196
+ testID: string;
197
+ }> = ({
198
+ label,
199
+ value,
200
+ onChange,
201
+ onComplete,
202
+ error,
203
+ disabled,
204
+ length,
205
+ type = 'numeric',
206
+ groupEvery,
207
+ autoFocus = true,
208
+ hint,
209
+ testID,
210
+ }) => (
211
+ <Field label={label} multiple description={hint} error={error} disabled={disabled}>
212
+ <InputOtp
213
+ length={length}
214
+ type={type}
215
+ groupEvery={groupEvery}
216
+ value={value}
217
+ onChange={onChange}
218
+ onComplete={onComplete}
219
+ invalid={error !== null}
220
+ disabled={disabled}
221
+ autoFocus={autoFocus}
222
+ testID={testID}
223
+ />
224
+ </Field>
225
+ );
226
+
180
227
  /** The screen's one primary action. */
181
228
  export const AccountFlowAction: React.FC<{
182
229
  label: string;
@@ -270,22 +317,17 @@ export const EmailCodeStep: React.FC<EmailCodeStepProps> = ({ description, verif
270
317
  return (
271
318
  <OxyAuthScreen>
272
319
  <OxyAuthScreenHeader title={t('emailCode.title')} description={description} />
273
- <AccountFlowField
320
+ <AccountFlowCodeField
274
321
  label={t('emailCode.label')}
275
322
  value={code}
276
323
  onChange={(value) => {
277
- const digits = value.replace(/\D/g, '').slice(0, EMAIL_CODE_LENGTH);
278
- setCode(digits);
324
+ setCode(value);
279
325
  if (error) setError(null);
280
- if (digits.length === EMAIL_CODE_LENGTH) confirm(digits);
281
326
  }}
282
- onSubmit={() => confirm(code)}
327
+ onComplete={confirm}
283
328
  error={error}
284
329
  disabled={pending}
285
- placeholder="000000"
286
- autoComplete="one-time-code"
287
- keyboardType="number-pad"
288
- maxLength={EMAIL_CODE_LENGTH}
330
+ length={EMAIL_CODE_LENGTH}
289
331
  testID="email-code"
290
332
  />
291
333
  {notice ? <Text style={[styles.note, { color: theme.colors.textSecondary }]}>{notice}</Text> : null}