@oxy.so/services 8.2.0 → 8.4.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.
- package/lib/commonjs/index.js +2 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/BloomLocaleBridge.js +37 -0
- package/lib/commonjs/ui/components/BloomLocaleBridge.js.map +1 -0
- package/lib/commonjs/ui/components/OxyProvider.js +16 -7
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/OxySignInRequestSurface.js +0 -1
- package/lib/commonjs/ui/components/OxySignInRequestSurface.js.map +1 -1
- package/lib/commonjs/ui/components/RequireOxyAuth.js +9 -1
- package/lib/commonjs/ui/components/RequireOxyAuth.js.map +1 -1
- package/lib/commonjs/ui/components/SurfaceScreen.js +4 -10
- package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/TroubleDisclosure.js +0 -1
- package/lib/commonjs/ui/components/authChooser/TroubleDisclosure.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/primitives.js +9 -11
- package/lib/commonjs/ui/components/authChooser/primitives.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/requestSurfaces.js +13 -10
- package/lib/commonjs/ui/components/authChooser/requestSurfaces.js.map +1 -1
- package/lib/commonjs/ui/components/authChooser/styles.js +4 -1
- package/lib/commonjs/ui/components/authChooser/styles.js.map +1 -1
- package/lib/commonjs/ui/components/payment/PaymentDetailsStep.js +2 -2
- package/lib/commonjs/ui/components/payment/PaymentDetailsStep.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/InlineCommonsQr.js +5 -2
- package/lib/commonjs/ui/components/signIn/InlineCommonsQr.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyAccountPicker.js +13 -17
- package/lib/commonjs/ui/components/signIn/OxyAccountPicker.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyAuthScreen.js +9 -8
- package/lib/commonjs/ui/components/signIn/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyAuthenticatorPanel.js +38 -42
- package/lib/commonjs/ui/components/signIn/OxyAuthenticatorPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js +21 -6
- package/lib/commonjs/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js +4 -2
- package/lib/commonjs/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxyPasswordPanel.js +5 -16
- package/lib/commonjs/ui/components/signIn/OxyPasswordPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxySignInPanel.js +19 -20
- package/lib/commonjs/ui/components/signIn/OxySignInPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxySignUpPanel.js +12 -8
- package/lib/commonjs/ui/components/signIn/OxySignUpPanel.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/ReauthStep.js +2 -7
- package/lib/commonjs/ui/components/signIn/ReauthStep.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/accountFlowParts.js +64 -13
- package/lib/commonjs/ui/components/signIn/accountFlowParts.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +25 -4
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/accountQueryPersistence.js +157 -0
- package/lib/commonjs/ui/hooks/accountQueryPersistence.js.map +1 -0
- package/lib/commonjs/ui/hooks/queryClient.js +17 -9
- package/lib/commonjs/ui/hooks/queryClient.js.map +1 -1
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/AppInfoScreen.js +2 -1
- package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/buildId.js +25 -0
- package/lib/commonjs/ui/utils/buildId.js.map +1 -0
- package/lib/commonjs/ui/utils/clipboard.js +21 -0
- package/lib/commonjs/ui/utils/clipboard.js.map +1 -0
- package/lib/commonjs/ui/utils/clipboard.native.js +28 -0
- package/lib/commonjs/ui/utils/clipboard.native.js.map +1 -0
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/BloomLocaleBridge.js +33 -0
- package/lib/module/ui/components/BloomLocaleBridge.js.map +1 -0
- package/lib/module/ui/components/OxyProvider.js +16 -7
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/OxySignInRequestSurface.js +0 -1
- package/lib/module/ui/components/OxySignInRequestSurface.js.map +1 -1
- package/lib/module/ui/components/RequireOxyAuth.js +9 -1
- package/lib/module/ui/components/RequireOxyAuth.js.map +1 -1
- package/lib/module/ui/components/SurfaceScreen.js +5 -11
- package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
- package/lib/module/ui/components/authChooser/TroubleDisclosure.js +0 -1
- package/lib/module/ui/components/authChooser/TroubleDisclosure.js.map +1 -1
- package/lib/module/ui/components/authChooser/primitives.js +11 -12
- package/lib/module/ui/components/authChooser/primitives.js.map +1 -1
- package/lib/module/ui/components/authChooser/requestSurfaces.js +15 -10
- package/lib/module/ui/components/authChooser/requestSurfaces.js.map +1 -1
- package/lib/module/ui/components/authChooser/styles.js +4 -1
- package/lib/module/ui/components/authChooser/styles.js.map +1 -1
- package/lib/module/ui/components/payment/PaymentDetailsStep.js +3 -3
- package/lib/module/ui/components/payment/PaymentDetailsStep.js.map +1 -1
- package/lib/module/ui/components/signIn/InlineCommonsQr.js +6 -3
- package/lib/module/ui/components/signIn/InlineCommonsQr.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyAccountPicker.js +13 -17
- package/lib/module/ui/components/signIn/OxyAccountPicker.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyAuthScreen.js +10 -9
- package/lib/module/ui/components/signIn/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyAuthenticatorPanel.js +40 -44
- package/lib/module/ui/components/signIn/OxyAuthenticatorPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js +22 -7
- package/lib/module/ui/components/signIn/OxyDeleteAccountPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js +5 -3
- package/lib/module/ui/components/signIn/OxyLinkCommonsPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/OxyPasswordPanel.js +6 -17
- package/lib/module/ui/components/signIn/OxyPasswordPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/OxySignInPanel.js +20 -21
- package/lib/module/ui/components/signIn/OxySignInPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/OxySignUpPanel.js +13 -9
- package/lib/module/ui/components/signIn/OxySignUpPanel.js.map +1 -1
- package/lib/module/ui/components/signIn/ReauthStep.js +3 -8
- package/lib/module/ui/components/signIn/ReauthStep.js.map +1 -1
- package/lib/module/ui/components/signIn/accountFlowParts.js +60 -11
- package/lib/module/ui/components/signIn/accountFlowParts.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +27 -6
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/accountQueryPersistence.js +153 -0
- package/lib/module/ui/hooks/accountQueryPersistence.js.map +1 -0
- package/lib/module/ui/hooks/queryClient.js +18 -10
- package/lib/module/ui/hooks/queryClient.js.map +1 -1
- package/lib/module/ui/index.js +3 -0
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/AppInfoScreen.js +3 -2
- package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
- package/lib/module/ui/utils/buildId.js +21 -0
- package/lib/module/ui/utils/buildId.js.map +1 -0
- package/lib/module/ui/utils/clipboard.js +17 -0
- package/lib/module/ui/utils/clipboard.js.map +1 -0
- package/lib/module/ui/utils/clipboard.native.js +23 -0
- package/lib/module/ui/utils/clipboard.native.js.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/BloomLocaleBridge.d.ts +19 -0
- package/lib/typescript/commonjs/ui/components/BloomLocaleBridge.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxySignInRequestSurface.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/RequireOxyAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/TroubleDisclosure.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/primitives.d.ts +4 -2
- package/lib/typescript/commonjs/ui/components/authChooser/primitives.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/requestSurfaces.d.ts +4 -4
- package/lib/typescript/commonjs/ui/components/authChooser/requestSurfaces.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/authChooser/styles.d.ts +2 -1
- package/lib/typescript/commonjs/ui/components/authChooser/styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/payment/PaymentDetailsStep.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/InlineCommonsQr.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyAccountPicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyAuthScreen.d.ts +1 -5
- package/lib/typescript/commonjs/ui/components/signIn/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyAuthenticatorPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxyPasswordPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxySignUpPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/ReauthStep.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts +29 -2
- package/lib/typescript/commonjs/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +9 -0
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/accountQueryPersistence.d.ts +54 -0
- package/lib/typescript/commonjs/ui/hooks/accountQueryPersistence.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/queryClient.d.ts +8 -2
- package/lib/typescript/commonjs/ui/hooks/queryClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/icons/Ionicons.d.ts +1 -1
- package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts +1 -0
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AppInfoScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +9 -0
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/buildId.d.ts +16 -0
- package/lib/typescript/commonjs/ui/utils/buildId.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/clipboard.d.ts +11 -0
- package/lib/typescript/commonjs/ui/utils/clipboard.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/clipboard.native.d.ts +18 -0
- package/lib/typescript/commonjs/ui/utils/clipboard.native.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/BloomLocaleBridge.d.ts +19 -0
- package/lib/typescript/module/ui/components/BloomLocaleBridge.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxySignInRequestSurface.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/RequireOxyAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/TroubleDisclosure.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/primitives.d.ts +4 -2
- package/lib/typescript/module/ui/components/authChooser/primitives.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/requestSurfaces.d.ts +4 -4
- package/lib/typescript/module/ui/components/authChooser/requestSurfaces.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/authChooser/styles.d.ts +2 -1
- package/lib/typescript/module/ui/components/authChooser/styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/payment/PaymentDetailsStep.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/InlineCommonsQr.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyAccountPicker.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyAuthScreen.d.ts +1 -5
- package/lib/typescript/module/ui/components/signIn/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyAuthenticatorPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyDeleteAccountPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyLinkCommonsPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxyPasswordPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxySignUpPanel.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/ReauthStep.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts +29 -2
- package/lib/typescript/module/ui/components/signIn/accountFlowParts.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +9 -0
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/accountQueryPersistence.d.ts +54 -0
- package/lib/typescript/module/ui/hooks/accountQueryPersistence.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/queryClient.d.ts +8 -2
- package/lib/typescript/module/ui/hooks/queryClient.d.ts.map +1 -1
- package/lib/typescript/module/ui/icons/Ionicons.d.ts +1 -1
- package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
- package/lib/typescript/module/ui/index.d.ts +1 -0
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AppInfoScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +9 -0
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/buildId.d.ts +16 -0
- package/lib/typescript/module/ui/utils/buildId.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/clipboard.d.ts +11 -0
- package/lib/typescript/module/ui/utils/clipboard.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/clipboard.native.d.ts +18 -0
- package/lib/typescript/module/ui/utils/clipboard.native.d.ts.map +1 -0
- package/package.json +11 -6
- package/src/index.ts +2 -0
- package/src/ui/components/BloomLocaleBridge.tsx +23 -0
- package/src/ui/components/OxyProvider.tsx +18 -9
- package/src/ui/components/OxySignInRequestSurface.tsx +0 -1
- package/src/ui/components/RequireOxyAuth.tsx +7 -2
- package/src/ui/components/SurfaceScreen.tsx +3 -9
- package/src/ui/components/__tests__/BloomLocaleBridge.test.tsx +68 -0
- package/src/ui/components/authChooser/TroubleDisclosure.tsx +0 -1
- package/src/ui/components/authChooser/primitives.tsx +11 -9
- package/src/ui/components/authChooser/requestSurfaces.tsx +9 -8
- package/src/ui/components/authChooser/styles.ts +4 -1
- package/src/ui/components/payment/PaymentDetailsStep.tsx +5 -3
- package/src/ui/components/signIn/InlineCommonsQr.tsx +5 -2
- package/src/ui/components/signIn/OxyAccountPicker.tsx +14 -13
- package/src/ui/components/signIn/OxyAuthScreen.tsx +5 -8
- package/src/ui/components/signIn/OxyAuthenticatorPanel.tsx +33 -45
- package/src/ui/components/signIn/OxyDeleteAccountPanel.tsx +17 -3
- package/src/ui/components/signIn/OxyLinkCommonsPanel.tsx +3 -2
- package/src/ui/components/signIn/OxyPasswordPanel.tsx +6 -12
- package/src/ui/components/signIn/OxySignInPanel.tsx +18 -15
- package/src/ui/components/signIn/OxySignUpPanel.tsx +13 -5
- package/src/ui/components/signIn/ReauthStep.tsx +4 -7
- package/src/ui/components/signIn/accountFlowParts.tsx +61 -7
- package/src/ui/context/OxyContext.tsx +35 -5
- package/src/ui/context/oxyContextTypes.ts +9 -0
- package/src/ui/hooks/accountQueryPersistence.ts +201 -0
- package/src/ui/hooks/queryClient.ts +17 -8
- package/src/ui/index.ts +2 -0
- package/src/ui/screens/AppInfoScreen.tsx +2 -2
- package/src/ui/types/navigation.ts +9 -0
- package/src/ui/utils/buildId.ts +18 -0
- package/src/ui/utils/clipboard.native.ts +20 -0
- package/src/ui/utils/clipboard.ts +14 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The pieces every Oxy sign-in and account screen shares: the error vocabulary,
|
|
3
3
|
* one labelled field, one one-time-code field, the one primary action, a note,
|
|
4
|
-
* and the email-code step.
|
|
4
|
+
* a notice, and the email-code step.
|
|
5
5
|
* Each screen is built from the same shell as `OxySignInPanel`
|
|
6
6
|
* (`OxyAuthScreen`), so they read as one product in the account dialog, in an
|
|
7
7
|
* app's settings and on auth.oxy.so.
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
import type React from 'react';
|
|
11
11
|
import { useState } from 'react';
|
|
12
12
|
import { StyleSheet, View } from 'react-native';
|
|
13
|
+
import { Admonition } from '@oxy.so/bloom/admonition';
|
|
13
14
|
import { Button } from '@oxy.so/bloom/button';
|
|
14
15
|
import { Field } from '@oxy.so/bloom/field';
|
|
15
16
|
import { InputOtp } from '@oxy.so/bloom/input-otp';
|
|
16
17
|
import { useTheme } from '@oxy.so/bloom/theme';
|
|
17
18
|
import { TextField, TextFieldHint, TextFieldInput, TextFieldLabel } from '@oxy.so/bloom/text-field';
|
|
18
19
|
import { Text } from '@oxy.so/bloom/typography';
|
|
20
|
+
import { WizardProgress } from '@oxy.so/bloom/wizard';
|
|
19
21
|
import {
|
|
20
22
|
EMAIL_CODE_LENGTH,
|
|
21
23
|
EMAIL_SIGNIN_LONG_CODE_ALPHABET,
|
|
@@ -119,7 +121,11 @@ export function isCompleteSignInCode(typed: string): boolean {
|
|
|
119
121
|
/** Characters in a backup code (`xxxxx-xxxxx`, its dash not counted). */
|
|
120
122
|
export const BACKUP_CODE_LENGTH = 10;
|
|
121
123
|
|
|
122
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* One labelled input, the way the sign-in screen draws its fields. A
|
|
126
|
+
* `secureTextEntry` field is always revealable: Bloom's eye button shows and
|
|
127
|
+
* hides the password, named in the locale Bloom speaks (`OxyProvider` sets it).
|
|
128
|
+
*/
|
|
123
129
|
export const AccountFlowField: React.FC<{
|
|
124
130
|
label: string;
|
|
125
131
|
value: string;
|
|
@@ -161,6 +167,7 @@ export const AccountFlowField: React.FC<{
|
|
|
161
167
|
autoComplete={autoComplete}
|
|
162
168
|
keyboardType={keyboardType}
|
|
163
169
|
secureTextEntry={secureTextEntry}
|
|
170
|
+
revealable={secureTextEntry}
|
|
164
171
|
autoCapitalize="none"
|
|
165
172
|
autoCorrect={false}
|
|
166
173
|
autoFocus={autoFocus}
|
|
@@ -257,6 +264,44 @@ export const AccountFlowNote: React.FC<{ children: React.ReactNode; testID?: str
|
|
|
257
264
|
);
|
|
258
265
|
};
|
|
259
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Something the person should know before acting, above the action it is
|
|
269
|
+
* about: a wait before trying again, a code that was just sent, a warning. A
|
|
270
|
+
* Bloom `Admonition`, not a field's error hint — nothing they typed is wrong.
|
|
271
|
+
*/
|
|
272
|
+
export const AccountFlowNotice: React.FC<{
|
|
273
|
+
children: React.ReactNode;
|
|
274
|
+
type?: 'info' | 'warning' | 'error';
|
|
275
|
+
testID?: string;
|
|
276
|
+
}> = ({ children, type = 'info', testID }) => (
|
|
277
|
+
<View testID={testID}>
|
|
278
|
+
<Admonition type={type}>{children}</Admonition>
|
|
279
|
+
</View>
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Where a step sits in a flow of several (sign-up, turning on the
|
|
284
|
+
* authenticator): Bloom's segmented `WizardProgress` and "Step 2 of 3". The
|
|
285
|
+
* screen's own header names the step, so the progress draws no title; the
|
|
286
|
+
* titles still name the bar for assistive technology.
|
|
287
|
+
*/
|
|
288
|
+
export const AccountFlowProgress: React.FC<{ steps: readonly string[]; current: number; testID?: string }> = ({
|
|
289
|
+
steps,
|
|
290
|
+
current,
|
|
291
|
+
testID,
|
|
292
|
+
}) => {
|
|
293
|
+
const { t } = useI18n();
|
|
294
|
+
return (
|
|
295
|
+
<WizardProgress
|
|
296
|
+
steps={steps.map((title, index) => ({ key: String(index), title }))}
|
|
297
|
+
current={current}
|
|
298
|
+
hideTitle
|
|
299
|
+
formatStepCount={(step, total) => t('common.stepOf', { step, total })}
|
|
300
|
+
testID={testID}
|
|
301
|
+
/>
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
|
|
260
305
|
/** Why a step failed, where it happened. */
|
|
261
306
|
export const AccountFlowErrorLine: React.FC<{ message: string }> = ({ message }) => {
|
|
262
307
|
const theme = useTheme();
|
|
@@ -277,11 +322,19 @@ export interface EmailCodeStepProps {
|
|
|
277
322
|
onResend: () => Promise<void>;
|
|
278
323
|
/** "Use another email" / "Back" — the step before. */
|
|
279
324
|
back: { label: string; onPress: () => void };
|
|
325
|
+
/** Where this step sits in a longer flow (a `WizardProgress`), under the header. */
|
|
326
|
+
progress?: React.ReactNode;
|
|
280
327
|
}
|
|
281
328
|
|
|
282
329
|
/** "Check your email": the 6-digit code an email verification sent. Submits itself once complete. */
|
|
283
|
-
export const EmailCodeStep: React.FC<EmailCodeStepProps> = ({
|
|
284
|
-
|
|
330
|
+
export const EmailCodeStep: React.FC<EmailCodeStepProps> = ({
|
|
331
|
+
description,
|
|
332
|
+
verificationId,
|
|
333
|
+
onConfirmed,
|
|
334
|
+
onResend,
|
|
335
|
+
back,
|
|
336
|
+
progress,
|
|
337
|
+
}) => {
|
|
285
338
|
const { t } = useI18n();
|
|
286
339
|
const { oxyServices } = useOxy();
|
|
287
340
|
const [code, setCode] = useState('');
|
|
@@ -317,6 +370,7 @@ export const EmailCodeStep: React.FC<EmailCodeStepProps> = ({ description, verif
|
|
|
317
370
|
return (
|
|
318
371
|
<OxyAuthScreen>
|
|
319
372
|
<OxyAuthScreenHeader title={t('emailCode.title')} description={description} />
|
|
373
|
+
{progress}
|
|
320
374
|
<AccountFlowCodeField
|
|
321
375
|
label={t('emailCode.label')}
|
|
322
376
|
value={code}
|
|
@@ -330,10 +384,10 @@ export const EmailCodeStep: React.FC<EmailCodeStepProps> = ({ description, verif
|
|
|
330
384
|
length={EMAIL_CODE_LENGTH}
|
|
331
385
|
testID="email-code"
|
|
332
386
|
/>
|
|
333
|
-
{notice ? <
|
|
387
|
+
{notice ? <AccountFlowNotice testID="email-code-notice">{notice}</AccountFlowNotice> : null}
|
|
334
388
|
<AccountFlowAction label={t('signin.actions.continue')} onPress={() => confirm(code)} pending={pending} testID="email-code-continue" />
|
|
335
|
-
<SubtleLink label={t('emailCode.resend')}
|
|
336
|
-
<SubtleLink label={back.label}
|
|
389
|
+
<SubtleLink label={t('emailCode.resend')} onPress={resend} disabled={pending} testID="email-code-resend" />
|
|
390
|
+
<SubtleLink label={back.label} onPress={back.onPress} disabled={pending} testID="email-code-back" />
|
|
337
391
|
</OxyAuthScreen>
|
|
338
392
|
);
|
|
339
393
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
useMemo,
|
|
8
8
|
useRef,
|
|
9
9
|
useState,
|
|
10
|
+
useSyncExternalStore,
|
|
10
11
|
} from 'react';
|
|
11
12
|
import { Linking, Platform } from 'react-native';
|
|
12
13
|
import { OxyServices } from '@oxy.so/core';
|
|
@@ -43,7 +44,7 @@ import { useLanguageManagement } from '../hooks/useLanguageManagement';
|
|
|
43
44
|
import { useSessionManagement } from '../hooks/useSessionManagement';
|
|
44
45
|
import { useAuthOperations, clearPersistedAuthSafe } from './hooks/useAuthOperations';
|
|
45
46
|
import { useDeviceManagement } from '../hooks/useDeviceManagement';
|
|
46
|
-
import { getStorageKeys, createPlatformStorage, type StorageInterface } from '../utils/storageHelpers';
|
|
47
|
+
import { getStorageKeys, createMemoryStorage, createPlatformStorage, type StorageInterface } from '../utils/storageHelpers';
|
|
47
48
|
import type { RouteName } from '../navigation/routes';
|
|
48
49
|
import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSheetManager';
|
|
49
50
|
import {
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
} from '../navigation/surfaces';
|
|
55
56
|
import { useQueryClient, onlineManager } from '@tanstack/react-query';
|
|
56
57
|
import { clearQueryCache } from '../hooks/queryClient';
|
|
58
|
+
import { createAccountQueryPersistence, type AccountQueryPersistence } from '../hooks/accountQueryPersistence';
|
|
57
59
|
import { useAvatarPicker } from '../hooks/useAvatarPicker';
|
|
58
60
|
import { resetSessionScopedStores } from '../stores/resetSessionScopedStores';
|
|
59
61
|
import { ASSET_DOWNLOAD_URLS_QUERY_KEY } from '../hooks/useResolvedFileUrls';
|
|
@@ -94,6 +96,8 @@ export type { LogoutResult, OxyContextState, OxyRuntimeProviderProps } from './o
|
|
|
94
96
|
|
|
95
97
|
const OxyRuntimeContext = createContext<OxyContextState | null>(null);
|
|
96
98
|
|
|
99
|
+
const subscribeNever = (): (() => void) => () => undefined;
|
|
100
|
+
|
|
97
101
|
type OxyAuthActionsContextState = Pick<
|
|
98
102
|
OxyContextState,
|
|
99
103
|
| 'signIn'
|
|
@@ -135,6 +139,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
135
139
|
webAuthMode = 'popup',
|
|
136
140
|
backgroundSession = false,
|
|
137
141
|
platformStorage,
|
|
142
|
+
accountQueries,
|
|
138
143
|
onAuthStateChange,
|
|
139
144
|
onError,
|
|
140
145
|
}) => {
|
|
@@ -283,6 +288,24 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
283
288
|
// Local display/bookkeeping storage (session id list, language). Distinct
|
|
284
289
|
// from `authStore` (the durable credential blob). Exposed as an awaitable so
|
|
285
290
|
// a persistence write is never dropped because it raced storage init.
|
|
291
|
+
const queryClient = useQueryClient();
|
|
292
|
+
|
|
293
|
+
// The app's per-account offline cache (`accountQueries`). Built once, like the
|
|
294
|
+
// runtime. `handleSubjectChange` activates it BEFORE anyone is woken, so a
|
|
295
|
+
// consumer of the new account never reads the previous account's rows.
|
|
296
|
+
const accountQueryPersistenceRef = useRef<AccountQueryPersistence | null | undefined>(undefined);
|
|
297
|
+
if (accountQueryPersistenceRef.current === undefined) {
|
|
298
|
+
accountQueryPersistenceRef.current = accountQueries
|
|
299
|
+
? createAccountQueryPersistence(queryClient, accountQueries)
|
|
300
|
+
: null;
|
|
301
|
+
}
|
|
302
|
+
const accountQueryPersistence = accountQueryPersistenceRef.current;
|
|
303
|
+
const isAccountCacheReady = useSyncExternalStore(
|
|
304
|
+
accountQueryPersistence ? accountQueryPersistence.subscribe : subscribeNever,
|
|
305
|
+
() => accountQueryPersistence?.isReady() ?? true,
|
|
306
|
+
() => accountQueryPersistence?.isReady() ?? true,
|
|
307
|
+
);
|
|
308
|
+
|
|
286
309
|
const storageRef = useRef<StorageInterface | null>(null);
|
|
287
310
|
const [storage, setStorage] = useState<StorageInterface | null>(null);
|
|
288
311
|
const buildStorageDeferred = () => {
|
|
@@ -313,12 +336,16 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
313
336
|
.then((storageInstance) => {
|
|
314
337
|
storageRef.current = storageInstance;
|
|
315
338
|
storageReady.resolve(storageInstance);
|
|
339
|
+
accountQueryPersistence?.setStorage(storageInstance);
|
|
316
340
|
if (mounted) {
|
|
317
341
|
setStorage(storageInstance);
|
|
318
342
|
runtime.setStorageReady(true);
|
|
319
343
|
}
|
|
320
344
|
})
|
|
321
345
|
.catch((err) => {
|
|
346
|
+
// Nothing persisted can be restored; the account cache still has to
|
|
347
|
+
// become ready, or RequireOxyAuth would wait forever.
|
|
348
|
+
accountQueryPersistence?.setStorage(createMemoryStorage());
|
|
322
349
|
if (mounted) {
|
|
323
350
|
logger('Failed to initialize storage', err);
|
|
324
351
|
onError?.({ message: 'Failed to initialize storage', code: 'STORAGE_INIT_ERROR', status: 500 });
|
|
@@ -327,7 +354,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
327
354
|
return () => {
|
|
328
355
|
mounted = false;
|
|
329
356
|
};
|
|
330
|
-
}, [logger, onError, platformStorage, runtime, storageReady]);
|
|
357
|
+
}, [accountQueryPersistence, logger, onError, platformStorage, runtime, storageReady]);
|
|
331
358
|
|
|
332
359
|
const {
|
|
333
360
|
currentLanguage,
|
|
@@ -344,8 +371,6 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
344
371
|
logger,
|
|
345
372
|
});
|
|
346
373
|
|
|
347
|
-
const queryClient = useQueryClient();
|
|
348
|
-
|
|
349
374
|
const {
|
|
350
375
|
saveSessionIds,
|
|
351
376
|
switchSession,
|
|
@@ -1207,6 +1232,9 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
1207
1232
|
// buy a refetch storm at first paint.
|
|
1208
1233
|
const handleSubjectChange = useCallback(
|
|
1209
1234
|
({ previous, next }: SubjectTransition): void => {
|
|
1235
|
+
// Every transition, the first sign-in included: the account cache is
|
|
1236
|
+
// keyed by the subject, and a sign-out deletes the signed-out one's.
|
|
1237
|
+
accountQueryPersistence?.activate(next);
|
|
1210
1238
|
if (previous === null) {
|
|
1211
1239
|
return;
|
|
1212
1240
|
}
|
|
@@ -1224,7 +1252,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
1224
1252
|
queryClient.invalidateQueries();
|
|
1225
1253
|
void refreshAccounts();
|
|
1226
1254
|
},
|
|
1227
|
-
[queryClient, refreshAccounts],
|
|
1255
|
+
[accountQueryPersistence, queryClient, refreshAccounts],
|
|
1228
1256
|
);
|
|
1229
1257
|
subjectChangeRef.current = handleSubjectChange;
|
|
1230
1258
|
|
|
@@ -1256,6 +1284,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
1256
1284
|
isPrivateApiPending,
|
|
1257
1285
|
isAuthResolved: authResolved,
|
|
1258
1286
|
isStorageReady: snapshot.storageReady,
|
|
1287
|
+
isAccountCacheReady,
|
|
1259
1288
|
sessionMode: isIdentityBound ? 'identity' : 'account',
|
|
1260
1289
|
webAuthMode,
|
|
1261
1290
|
error,
|
|
@@ -1310,6 +1339,7 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
1310
1339
|
isPrivateApiPending,
|
|
1311
1340
|
authResolved,
|
|
1312
1341
|
snapshot.storageReady,
|
|
1342
|
+
isAccountCacheReady,
|
|
1313
1343
|
isIdentityBound,
|
|
1314
1344
|
webAuthMode,
|
|
1315
1345
|
error,
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from '../oauth/explicitOAuthConsent';
|
|
13
13
|
import type { WebAuthMode, WebOAuthSignInResult } from '../oauth/types';
|
|
14
14
|
import type { StorageInterface } from '../utils/storageHelpers';
|
|
15
|
+
import type { AccountQueriesConfig } from '../hooks/accountQueryPersistence';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* The outcome of `logout()`. It never rejects: a caller reads this instead.
|
|
@@ -48,6 +49,12 @@ export interface OxyContextState {
|
|
|
48
49
|
*/
|
|
49
50
|
isAuthResolved: boolean;
|
|
50
51
|
isStorageReady: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Whether the signed-in account's persisted `accountQueries` cache has been
|
|
54
|
+
* restored. Always `true` signed out or when the app declared none.
|
|
55
|
+
* `RequireOxyAuth` waits on it.
|
|
56
|
+
*/
|
|
57
|
+
isAccountCacheReady: boolean;
|
|
51
58
|
/**
|
|
52
59
|
* Who owns this provider's session (see `OxyProviderProps.sessionMode`).
|
|
53
60
|
*
|
|
@@ -200,6 +207,8 @@ export interface OxyRuntimeProviderProps {
|
|
|
200
207
|
onError?: (error: ApiError) => void;
|
|
201
208
|
/** Storage instance owned by the public provider; internal composition seam. */
|
|
202
209
|
platformStorage?: StorageInterface | null;
|
|
210
|
+
/** See `OxyProviderProps.accountQueries`. Read once, at mount. */
|
|
211
|
+
accountQueries?: AccountQueriesConfig;
|
|
203
212
|
}
|
|
204
213
|
|
|
205
214
|
/** Internal commit input — session plus zero-cookie device credential. */
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline persistence for an app's PRIVATE, per-account queries.
|
|
3
|
+
*
|
|
4
|
+
* The SDK's own persister (`attachQueryPersistence`) keeps account-agnostic
|
|
5
|
+
* data. An app's mail, library or drafts belong to one account, and every app
|
|
6
|
+
* that wanted them to survive a restart had to scope a persister to the session
|
|
7
|
+
* itself. The SDK owns the session, so it owns this too:
|
|
8
|
+
*
|
|
9
|
+
* - Storage is keyed by the account (`oxy_account_queries:<accountId>`), so a
|
|
10
|
+
* restore can only ever hydrate the account that is signed in.
|
|
11
|
+
* - The buster is the build (`getOxyBuildId`), so no cache written by an older
|
|
12
|
+
* bundle, in an older shape, is ever served.
|
|
13
|
+
* - On a subject change the declared roots, memory-only ones included, and the
|
|
14
|
+
* account's paused mutations are dropped
|
|
15
|
+
* from memory synchronously, before anyone is woken, and the new account's
|
|
16
|
+
* blob is restored. `isReady()` is false until that restore settles, and
|
|
17
|
+
* `RequireOxyAuth` waits on it, so another account's rows never render.
|
|
18
|
+
* - On sign-out the signed-out account's blob is deleted.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { hashKey, type Mutation, type Query, type QueryClient, type QueryKey } from '@tanstack/react-query';
|
|
22
|
+
import { persistQueryClient } from '@tanstack/react-query-persist-client';
|
|
23
|
+
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
|
|
24
|
+
import type { StorageInterface } from '../utils/storageHelpers';
|
|
25
|
+
import { getOxyBuildId } from '../utils/buildId';
|
|
26
|
+
|
|
27
|
+
/** An app's declaration of which data is private to the signed-in account. */
|
|
28
|
+
export interface AccountQueriesConfig {
|
|
29
|
+
/**
|
|
30
|
+
* Query-key roots (`queryKey[0]`) whose data belongs to the signed-in account
|
|
31
|
+
* and survives a restart.
|
|
32
|
+
*/
|
|
33
|
+
roots: readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* Roots that belong to the account too, but must not be written to disk
|
|
36
|
+
* (signed URLs, AI output, search results): dropped on a switch, never persisted.
|
|
37
|
+
*/
|
|
38
|
+
memoryOnlyRoots?: readonly string[];
|
|
39
|
+
/**
|
|
40
|
+
* Mutation keys whose PAUSED (offline) instances replay after a restart for
|
|
41
|
+
* the same account. Register their `mutationFn` with `setMutationDefaults`.
|
|
42
|
+
*/
|
|
43
|
+
mutationKeys?: readonly QueryKey[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const ACCOUNT_QUERY_CACHE_KEY = 'oxy_account_queries';
|
|
47
|
+
const ACCOUNT_QUERY_CACHE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
48
|
+
const ACCOUNT_QUERY_PERSIST_THROTTLE_MS = 1_000;
|
|
49
|
+
|
|
50
|
+
export interface AccountQueryPersistence {
|
|
51
|
+
/** Storage became available (it initializes asynchronously). */
|
|
52
|
+
setStorage(storage: StorageInterface): void;
|
|
53
|
+
/** The signed-in account changed. `null` is a sign-out. Synchronous in-memory reset. */
|
|
54
|
+
activate(accountId: string | null): void;
|
|
55
|
+
/** True once the active account's cache is restored (always true signed out). */
|
|
56
|
+
isReady(): boolean;
|
|
57
|
+
subscribe(listener: () => void): () => void;
|
|
58
|
+
/** Whether a mutation is account-scoped (so the SDK's own persister skips it). */
|
|
59
|
+
ownsMutation(mutation: Mutation): boolean;
|
|
60
|
+
dispose(): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function storageKeyFor(accountId: string): string {
|
|
64
|
+
return `${ACCOUNT_QUERY_CACHE_KEY}:${encodeURIComponent(accountId)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createAccountQueryPersistence(
|
|
68
|
+
queryClient: QueryClient,
|
|
69
|
+
config: AccountQueriesConfig,
|
|
70
|
+
): AccountQueryPersistence {
|
|
71
|
+
const persistedRoots = new Set(config.roots);
|
|
72
|
+
const accountRoots = new Set([...config.roots, ...(config.memoryOnlyRoots ?? [])]);
|
|
73
|
+
const mutationHashes = new Set((config.mutationKeys ?? []).map((key) => hashKey(key)));
|
|
74
|
+
const listeners = new Set<() => void>();
|
|
75
|
+
|
|
76
|
+
let storage: StorageInterface | null = null;
|
|
77
|
+
let accountId: string | null = null;
|
|
78
|
+
let restoredFor: string | null = null;
|
|
79
|
+
let generation = 0;
|
|
80
|
+
let unsubscribePersist: (() => void) | null = null;
|
|
81
|
+
// The in-flight restore. A new account waits for it, so a late restore can
|
|
82
|
+
// never hydrate the previous account's rows after the switch.
|
|
83
|
+
let restoring: Promise<void> = Promise.resolve();
|
|
84
|
+
|
|
85
|
+
const notify = () => {
|
|
86
|
+
for (const listener of listeners) listener();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const rootOf = (query: Query): string | null => {
|
|
90
|
+
const head = query.queryKey[0];
|
|
91
|
+
return typeof head === 'string' ? head : null;
|
|
92
|
+
};
|
|
93
|
+
const ownsQuery = (query: Query): boolean => accountRoots.has(rootOf(query) ?? '');
|
|
94
|
+
const persistsQuery = (query: Query): boolean => persistedRoots.has(rootOf(query) ?? '');
|
|
95
|
+
|
|
96
|
+
const ownsMutation = (mutation: Mutation): boolean => {
|
|
97
|
+
const key = mutation.options.mutationKey;
|
|
98
|
+
return key !== undefined && mutationHashes.has(hashKey(key));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const dropScopedData = () => {
|
|
102
|
+
queryClient.removeQueries({ predicate: ownsQuery });
|
|
103
|
+
const mutations = queryClient.getMutationCache();
|
|
104
|
+
for (const mutation of mutations.getAll()) {
|
|
105
|
+
if (ownsMutation(mutation)) mutations.remove(mutation);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const persisterFor = (id: string, target: StorageInterface) =>
|
|
110
|
+
createAsyncStoragePersister({
|
|
111
|
+
storage: {
|
|
112
|
+
getItem: (key) => target.getItem(key),
|
|
113
|
+
setItem: (key, value) => target.setItem(key, value),
|
|
114
|
+
removeItem: (key) => target.removeItem(key),
|
|
115
|
+
},
|
|
116
|
+
key: storageKeyFor(id),
|
|
117
|
+
throttleTime: ACCOUNT_QUERY_PERSIST_THROTTLE_MS,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const start = () => {
|
|
121
|
+
const id = accountId;
|
|
122
|
+
const target = storage;
|
|
123
|
+
if (!id || !target || unsubscribePersist) return;
|
|
124
|
+
const current = generation;
|
|
125
|
+
const [unsubscribe, restored] = persistQueryClient({
|
|
126
|
+
queryClient,
|
|
127
|
+
persister: persisterFor(id, target),
|
|
128
|
+
maxAge: ACCOUNT_QUERY_CACHE_MAX_AGE,
|
|
129
|
+
buster: getOxyBuildId(),
|
|
130
|
+
dehydrateOptions: {
|
|
131
|
+
shouldDehydrateQuery: (query) => query.state.status === 'success' && persistsQuery(query),
|
|
132
|
+
shouldDehydrateMutation: (mutation) => mutation.state.isPaused && ownsMutation(mutation),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
unsubscribePersist = unsubscribe;
|
|
136
|
+
restoring = restored.catch(() => undefined).then(() => {
|
|
137
|
+
if (current !== generation) return;
|
|
138
|
+
restoredFor = id;
|
|
139
|
+
notify();
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const stop = () => {
|
|
144
|
+
unsubscribePersist?.();
|
|
145
|
+
unsubscribePersist = null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
setStorage(next) {
|
|
150
|
+
if (storage) return;
|
|
151
|
+
storage = next;
|
|
152
|
+
const current = generation;
|
|
153
|
+
void restoring.then(() => {
|
|
154
|
+
if (current === generation) start();
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
activate(next) {
|
|
159
|
+
if (next === accountId) return;
|
|
160
|
+
const previous = accountId;
|
|
161
|
+
generation += 1;
|
|
162
|
+
const current = generation;
|
|
163
|
+
stop();
|
|
164
|
+
dropScopedData();
|
|
165
|
+
accountId = next;
|
|
166
|
+
restoredFor = null;
|
|
167
|
+
notify();
|
|
168
|
+
|
|
169
|
+
if (next === null && previous !== null && storage) {
|
|
170
|
+
void persisterFor(previous, storage).removeClient();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
void restoring.then(() => {
|
|
174
|
+
if (current !== generation) return;
|
|
175
|
+
// A restore that was already in flight may have hydrated the previous
|
|
176
|
+
// account's rows; drop them again before this account's restore.
|
|
177
|
+
dropScopedData();
|
|
178
|
+
start();
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
isReady() {
|
|
183
|
+
return accountId === null || restoredFor === accountId;
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
subscribe(listener) {
|
|
187
|
+
listeners.add(listener);
|
|
188
|
+
return () => {
|
|
189
|
+
listeners.delete(listener);
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
ownsMutation,
|
|
194
|
+
|
|
195
|
+
dispose() {
|
|
196
|
+
generation += 1;
|
|
197
|
+
stop();
|
|
198
|
+
listeners.clear();
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* when introducing reads that should survive restart.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { QueryClient, onlineManager, type Query, type Mutation } from '@tanstack/react-query';
|
|
27
|
+
import { QueryClient, hashKey, onlineManager, type Query, type Mutation, type QueryKey } from '@tanstack/react-query';
|
|
28
28
|
import {
|
|
29
29
|
persistQueryClient,
|
|
30
30
|
type PersistedClient,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
|
|
33
33
|
import { isDev } from '@oxy.so/core';
|
|
34
34
|
import type { StorageInterface } from '../utils/storageHelpers';
|
|
35
|
+
import { getOxyBuildId } from '../utils/buildId';
|
|
35
36
|
|
|
36
37
|
const QUERY_CACHE_KEY = 'oxy_query_cache_v3';
|
|
37
38
|
const QUERY_CACHE_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
@@ -106,13 +107,6 @@ function shouldDehydrateQuery(query: Query): boolean {
|
|
|
106
107
|
return PERSISTED_QUERY_PREFIXES.includes(head);
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
/**
|
|
110
|
-
* Persist every mutation regardless of status — paused mutations are
|
|
111
|
-
* exactly the ones that must survive restart to replay when online.
|
|
112
|
-
*/
|
|
113
|
-
function shouldDehydrateMutation(_mutation: Mutation): boolean {
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
110
|
|
|
117
111
|
/**
|
|
118
112
|
* Create a QueryClient with offline-first defaults.
|
|
@@ -191,6 +185,12 @@ export interface AttachPersistenceResult {
|
|
|
191
185
|
export const attachQueryPersistence = (
|
|
192
186
|
queryClient: QueryClient,
|
|
193
187
|
storage: StorageInterface | null | undefined,
|
|
188
|
+
/**
|
|
189
|
+
* Mutation keys the app declared account-scoped (`accountQueries`). Those are
|
|
190
|
+
* persisted per account by `createAccountQueryPersistence`, never here, so a
|
|
191
|
+
* paused mutation cannot replay twice or under another account.
|
|
192
|
+
*/
|
|
193
|
+
accountMutationKeys: readonly QueryKey[] = [],
|
|
194
194
|
): AttachPersistenceResult => {
|
|
195
195
|
if (!storage) {
|
|
196
196
|
return {
|
|
@@ -205,10 +205,19 @@ export const attachQueryPersistence = (
|
|
|
205
205
|
throttleTime: QUERY_PERSIST_THROTTLE_MS,
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
+
// Persist every other mutation regardless of status: paused mutations are
|
|
209
|
+
// exactly the ones that must survive restart to replay when online.
|
|
210
|
+
const accountScoped = new Set(accountMutationKeys.map((key) => hashKey(key)));
|
|
211
|
+
const shouldDehydrateMutation = (mutation: Mutation): boolean => {
|
|
212
|
+
const key = mutation.options.mutationKey;
|
|
213
|
+
return key === undefined || !accountScoped.has(hashKey(key));
|
|
214
|
+
};
|
|
215
|
+
|
|
208
216
|
const [unsubscribe, restored] = persistQueryClient({
|
|
209
217
|
queryClient,
|
|
210
218
|
persister,
|
|
211
219
|
maxAge: QUERY_CACHE_MAX_AGE,
|
|
220
|
+
buster: getOxyBuildId(),
|
|
212
221
|
dehydrateOptions: {
|
|
213
222
|
shouldDehydrateQuery,
|
|
214
223
|
shouldDehydrateMutation,
|
package/src/ui/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { LogoIcon } from './components/logo/LogoIcon';
|
|
|
24
24
|
export { LogoText } from './components/logo/LogoText';
|
|
25
25
|
export { RequireOxyAuth } from './components/RequireOxyAuth';
|
|
26
26
|
export type { RequireOxyAuthProps, RequireOxyAuthPrompt } from './components/RequireOxyAuth';
|
|
27
|
+
// `OxyProvider`'s `accountQueries`: the app's per-account offline cache.
|
|
28
|
+
export type { AccountQueriesConfig } from './hooks/accountQueryPersistence';
|
|
27
29
|
export { default as FollowButton } from './components/FollowButton';
|
|
28
30
|
export { default as PeableButton } from './components/PeableButton';
|
|
29
31
|
export type { PeableButtonProps } from './components/PeableButton';
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
View,
|
|
5
5
|
Platform,
|
|
6
6
|
Dimensions,
|
|
7
|
-
Clipboard,
|
|
8
7
|
ActivityIndicator,
|
|
9
8
|
} from 'react-native';
|
|
10
9
|
import type { BaseScreenProps } from '../types/navigation';
|
|
11
10
|
import { packageInfo } from '@oxy.so/core';
|
|
12
11
|
import { toast } from '@oxy.so/bloom/toast';
|
|
12
|
+
import { copyText } from '../utils/clipboard';
|
|
13
13
|
import OxyServicesLogo from '../../assets/icons/OxyServices';
|
|
14
14
|
import { SettingsIcon } from '../components/SettingsIcon';
|
|
15
15
|
import { useTheme } from '@oxy.so/bloom/theme';
|
|
@@ -93,7 +93,7 @@ const AppInfoScreen: React.FC<BaseScreenProps> = ({
|
|
|
93
93
|
|
|
94
94
|
const copyToClipboard = async (text: string, label: string) => {
|
|
95
95
|
try {
|
|
96
|
-
await
|
|
96
|
+
await copyText(text);
|
|
97
97
|
toast.success(t('appInfo.toasts.copiedToClipboard', { label }));
|
|
98
98
|
} catch (error) {
|
|
99
99
|
toast.error(t('appInfo.toasts.copyFailed'));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode, RefObject } from 'react';
|
|
2
2
|
import type { QueryClient } from '@tanstack/react-query';
|
|
3
|
+
import type { AccountQueriesConfig } from '../hooks/accountQueryPersistence';
|
|
3
4
|
import type { RouteName } from '../navigation/routes';
|
|
4
5
|
import type { User } from '@oxy.so/core';
|
|
5
6
|
import type { ClientSession } from '@oxy.so/core';
|
|
@@ -131,6 +132,14 @@ export interface OxyProviderProps {
|
|
|
131
132
|
*/
|
|
132
133
|
webAuthMode?: WebAuthMode;
|
|
133
134
|
queryClient?: QueryClient;
|
|
135
|
+
/**
|
|
136
|
+
* The app's private, per-account data, persisted offline by the SDK: stored
|
|
137
|
+
* per account, discarded by every new build, dropped from memory on an
|
|
138
|
+
* account switch and deleted on sign-out. `RequireOxyAuth` waits for the
|
|
139
|
+
* signed-in account's cache to restore, so another account's rows never
|
|
140
|
+
* render. Works with the SDK's client or one passed as `queryClient`.
|
|
141
|
+
*/
|
|
142
|
+
accountQueries?: AccountQueriesConfig;
|
|
134
143
|
/**
|
|
135
144
|
* Convenience: wrap the whole app subtree in `<RequireOxyAuth prompt=...>`.
|
|
136
145
|
* `off` (default) renders children unconditionally; `soft` adds a dismissible
|
|
@@ -0,0 +1,18 @@
|
|
|
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 function getOxyBuildId(): string {
|
|
16
|
+
const buildId = typeof process === 'undefined' ? undefined : process.env.OXY_BUILD_ID;
|
|
17
|
+
return buildId || 'unversioned';
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put text on the clipboard on iOS and Android, through `expo-clipboard`.
|
|
3
|
+
* React Native's own `Clipboard` is deprecated and gone from newer releases.
|
|
4
|
+
*
|
|
5
|
+
* `expo-clipboard` is an OPTIONAL peer, loaded with `import()` so an app
|
|
6
|
+
* without it (or with the JS but not the native module, before a rebuild)
|
|
7
|
+
* still bundles and boots: the copy rejects here and the caller says nothing
|
|
8
|
+
* was copied. A static import would instead fail the Metro build or throw
|
|
9
|
+
* while the root barrel evaluates.
|
|
10
|
+
*
|
|
11
|
+
* Only this NATIVE fork names the package; the default fork (`clipboard.ts`,
|
|
12
|
+
* which `tsc`, Vite and SSR resolve) names nothing, so a consumer's type graph
|
|
13
|
+
* reaches `expo-clipboard` only when it typechecks `.native` files
|
|
14
|
+
* (`moduleSuffixes`) — the case package-rules.md#package-boundaries warns
|
|
15
|
+
* about. `clipboardIsolation.test.ts` keeps it that way.
|
|
16
|
+
*/
|
|
17
|
+
export async function copyText(text: string): Promise<void> {
|
|
18
|
+
const clipboard = await import('expo-clipboard');
|
|
19
|
+
await clipboard.setStringAsync(text);
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put text on the clipboard: the browser's asynchronous Clipboard API. Rejects
|
|
3
|
+
* where there is none (an insecure origin, SSR, a denied permission), so the
|
|
4
|
+
* caller reports only a copy that happened.
|
|
5
|
+
*
|
|
6
|
+
* The native fork (`clipboard.native.ts`) uses `expo-clipboard`. React
|
|
7
|
+
* Native's own `Clipboard` is deprecated and gone from newer releases, so
|
|
8
|
+
* neither fork reaches for it.
|
|
9
|
+
*/
|
|
10
|
+
export async function copyText(text: string): Promise<void> {
|
|
11
|
+
const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard;
|
|
12
|
+
if (!clipboard?.writeText) throw new Error('clipboard unavailable');
|
|
13
|
+
await clipboard.writeText(text);
|
|
14
|
+
}
|