@oxy.so/services 13.5.0 → 13.7.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/ui/components/AccountLanguageBoundary.js +35 -0
- package/lib/commonjs/ui/components/AccountLanguageBoundary.js.map +1 -0
- package/lib/commonjs/ui/components/LanguageBridge.js +37 -34
- package/lib/commonjs/ui/components/LanguageBridge.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +19 -26
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/signIn/OxySignUpPanel.js +8 -2
- package/lib/commonjs/ui/components/signIn/OxySignUpPanel.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +19 -2
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useEntryLink.js +97 -0
- package/lib/commonjs/ui/hooks/useEntryLink.js.map +1 -0
- package/lib/commonjs/ui/hooks/useLanguageManagement.js +45 -14
- package/lib/commonjs/ui/hooks/useLanguageManagement.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountMembersScreen.js +8 -1
- package/lib/commonjs/ui/screens/AccountMembersScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/entryLink.js +54 -0
- package/lib/commonjs/ui/utils/entryLink.js.map +1 -0
- package/lib/module/ui/components/AccountLanguageBoundary.js +32 -0
- package/lib/module/ui/components/AccountLanguageBoundary.js.map +1 -0
- package/lib/module/ui/components/LanguageBridge.js +38 -35
- package/lib/module/ui/components/LanguageBridge.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +18 -27
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/signIn/OxySignUpPanel.js +8 -2
- package/lib/module/ui/components/signIn/OxySignUpPanel.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +19 -2
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useEntryLink.js +94 -0
- package/lib/module/ui/hooks/useEntryLink.js.map +1 -0
- package/lib/module/ui/hooks/useLanguageManagement.js +45 -14
- package/lib/module/ui/hooks/useLanguageManagement.js.map +1 -1
- package/lib/module/ui/screens/AccountMembersScreen.js +8 -1
- package/lib/module/ui/screens/AccountMembersScreen.js.map +1 -1
- package/lib/module/ui/utils/entryLink.js +49 -0
- package/lib/module/ui/utils/entryLink.js.map +1 -0
- package/lib/typescript/commonjs/ui/components/AccountLanguageBoundary.d.ts +8 -0
- package/lib/typescript/commonjs/ui/components/AccountLanguageBoundary.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/LanguageBridge.d.ts +7 -17
- package/lib/typescript/commonjs/ui/components/LanguageBridge.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/signIn/OxySignUpPanel.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 +5 -0
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useEntryLink.d.ts +15 -0
- package/lib/typescript/commonjs/ui/hooks/useEntryLink.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useLanguageManagement.d.ts +5 -1
- package/lib/typescript/commonjs/ui/hooks/useLanguageManagement.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +4 -0
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/entryLink.d.ts +8 -0
- package/lib/typescript/commonjs/ui/utils/entryLink.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/AccountLanguageBoundary.d.ts +8 -0
- package/lib/typescript/module/ui/components/AccountLanguageBoundary.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/LanguageBridge.d.ts +7 -17
- package/lib/typescript/module/ui/components/LanguageBridge.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/signIn/OxySignUpPanel.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 +5 -0
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useEntryLink.d.ts +15 -0
- package/lib/typescript/module/ui/hooks/useEntryLink.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useLanguageManagement.d.ts +5 -1
- package/lib/typescript/module/ui/hooks/useLanguageManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +4 -0
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/entryLink.d.ts +8 -0
- package/lib/typescript/module/ui/utils/entryLink.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/ui/components/AccountLanguageBoundary.tsx +35 -0
- package/src/ui/components/LanguageBridge.tsx +43 -39
- package/src/ui/components/OxyProvider.tsx +18 -24
- package/src/ui/components/__tests__/AccountLanguageBoundary.test.tsx +87 -0
- package/src/ui/components/__tests__/LanguageBridge.test.tsx +80 -60
- package/src/ui/components/signIn/OxySignUpPanel.tsx +9 -1
- package/src/ui/context/OxyContext.tsx +21 -1
- package/src/ui/context/oxyContextTypes.ts +5 -0
- package/src/ui/hooks/useEntryLink.ts +103 -0
- package/src/ui/hooks/useLanguageManagement.ts +53 -18
- package/src/ui/screens/AccountMembersScreen.tsx +8 -1
- package/src/ui/types/navigation.ts +4 -0
- package/src/ui/utils/__tests__/entryLink.test.ts +30 -0
- package/src/ui/utils/entryLink.ts +61 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CampaignAttribution } from '@oxy.so/contracts/client';
|
|
2
|
+
export declare function readCampaignAttribution(value: unknown): CampaignAttribution | undefined;
|
|
3
|
+
/** Consumes only explicit locale/campaign parameters, never the URL or referrer itself. */
|
|
4
|
+
export declare function readEntryLink(url: string | null): {
|
|
5
|
+
language?: string;
|
|
6
|
+
campaignAttribution?: CampaignAttribution;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=entryLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entryLink.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/entryLink.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,0BAA0B,CAAC;AAWlC,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,GAAG,SAAS,CAavF;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C,CAuBA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/services",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.7.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",
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"delete-dts.js": "find ./lib/commonjs -name '*.d.js*' -delete && find ./lib/module -name '*.d.js*' -delete"
|
|
152
152
|
},
|
|
153
153
|
"dependencies": {
|
|
154
|
-
"@oxy.so/contracts": "^6.
|
|
154
|
+
"@oxy.so/contracts": "^6.1.0",
|
|
155
155
|
"color": "^5.0.3",
|
|
156
156
|
"@peable.to/sdk": "0.4.1"
|
|
157
157
|
},
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"@commitlint/config-conventional": "^21.2.0",
|
|
161
161
|
"@expo/app-integrity": "~58.0.2",
|
|
162
162
|
"@expo/vector-icons": "^15.0.3",
|
|
163
|
-
"@oxy.so/core": "7.
|
|
163
|
+
"@oxy.so/core": "7.6.0",
|
|
164
164
|
"@react-native-async-storage/async-storage": "^2.0.0",
|
|
165
165
|
"@react-native-community/netinfo": "^12.0.1",
|
|
166
166
|
"@react-native/babel-preset": "^0.88.0",
|
|
@@ -210,13 +210,13 @@
|
|
|
210
210
|
"expo-video": "~58.0.12"
|
|
211
211
|
},
|
|
212
212
|
"//@oxy.so/bloom": "Bloom 11.1.0 adds the shared grouped sections, usage card, subscription plans, frame appearance and overlay navigation APIs used by this SDK. First-party Bloom peers remain open above this tested minimum.",
|
|
213
|
-
"//@oxy.so/core": "Core 7.2.0 supplies `assets.cachedUrl`, with which `useFileDownloadUrl` answers an already-resolved URL in its first render; 6.16.0 supplies `requireServerVerdict` and `onHolderRejected`, with which the cold boot checks a session it painted from the cache; 6.13.0 supplies the `accounts.settings.*` strings (picture, categories) the account settings speak in every language; 6.12.0 supplies `fileManagement.contentLost` and the `lost` file status a lost file is listed with; 6.11.0 supplies the `follow.errors.*` strings the follow error codes are worded with; 6.4.0 the `follow.*` strings the follow buttons read for every verb and language; 6.1.0 the `oxy.auth.email.{startChange,change,remove}` and `oxy.auth.password.remove` the sign-in & security panels call. Keep one Core instance per app; raise this floor with the root catalogue when new runtime APIs are required.",
|
|
213
|
+
"//@oxy.so/core": "Core 7.6.0 forwards guest locale and first-touch campaign metadata through signup; 7.3.0 supplies `users.lookupMany`, with which the account members screen fails (and retries) a profile read whose chunk failed instead of caching those members as handle-less; 7.2.0 supplies `assets.cachedUrl`, with which `useFileDownloadUrl` answers an already-resolved URL in its first render; 6.16.0 supplies `requireServerVerdict` and `onHolderRejected`, with which the cold boot checks a session it painted from the cache; 6.13.0 supplies the `accounts.settings.*` strings (picture, categories) the account settings speak in every language; 6.12.0 supplies `fileManagement.contentLost` and the `lost` file status a lost file is listed with; 6.11.0 supplies the `follow.errors.*` strings the follow error codes are worded with; 6.4.0 the `follow.*` strings the follow buttons read for every verb and language; 6.1.0 the `oxy.auth.email.{startChange,change,remove}` and `oxy.auth.password.remove` the sign-in & security panels call. Keep one Core instance per app; raise this floor with the root catalogue when new runtime APIs are required.",
|
|
214
214
|
"//react-native": "React Native 0.81 to 0.88, React 19.1+ and Expo SDK 54 to 58. The floors are the Expo SDK 54 stack (react-native 0.81.6, react 19.1.4, react-native-web 0.21.4, expo 54.0.38 and the module versions its bundledNativeModules.json pins), which compat/expo-54 installs as an external consumer and checks on every CI run (`bun check.mjs`): every peer here admits that stack, services' published src typechecks against its types, and <OxyProvider> mounts and settles its cold boot on iOS (React Native 0.81), web (react-native-web 0.21) and react-native-macos 0.81 with the native modules macOS lacks absent. Astro, Oxy's browser, runs react-native-macos 0.81 (0.83 is its latest release), which is why the floor is not 0.85. The SDK 56 floors these replaced were the versions current when the ranges were first written, not measurements.",
|
|
215
215
|
"peerDependencies": {
|
|
216
216
|
"@expo/app-integrity": ">=0.1.10 <59.0.0",
|
|
217
217
|
"@expo/vector-icons": "^15.0.3",
|
|
218
218
|
"@oxy.so/bloom": ">=11.1.0",
|
|
219
|
-
"@oxy.so/core": "^7.
|
|
219
|
+
"@oxy.so/core": "^7.6.0",
|
|
220
220
|
"@react-native-async-storage/async-storage": "^2.0.0",
|
|
221
221
|
"@react-native-community/netinfo": ">=11.4.1 <13.0.0",
|
|
222
222
|
"@tanstack/query-async-storage-persister": "^5.101",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { FALLBACK_LOCALE, getPrimaryLanguage } from '@oxy.so/core';
|
|
3
|
+
import { useOxy } from '../context/oxyRuntimeContext';
|
|
4
|
+
import { useCurrentUser } from '../hooks/queries/useAccountQueries';
|
|
5
|
+
import type { OxyLanguageConfig } from '../types/navigation';
|
|
6
|
+
import { LanguageBridge } from './LanguageBridge';
|
|
7
|
+
|
|
8
|
+
/** Owner preferences are not part of the public device/account summary. */
|
|
9
|
+
export function AccountLanguageBoundary({
|
|
10
|
+
language,
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
language?: OxyLanguageConfig;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}) {
|
|
16
|
+
const { canUsePrivateApi, user, currentLanguage } = useOxy();
|
|
17
|
+
const profile = useCurrentUser({ enabled: canUsePrivateApi });
|
|
18
|
+
// The query mirrors its result into the account runtime in an effect. Wait for
|
|
19
|
+
// that projection as well as the request, so the bridge never publishes the
|
|
20
|
+
// compact session's fallback locale in the frame before its real preference.
|
|
21
|
+
const accountReady =
|
|
22
|
+
!user ||
|
|
23
|
+
!canUsePrivateApi ||
|
|
24
|
+
profile.fetchStatus === 'paused' ||
|
|
25
|
+
user.languages !== undefined ||
|
|
26
|
+
profile.isError ||
|
|
27
|
+
(profile.dataUpdatedAt > 0 &&
|
|
28
|
+
profile.data?.id === user.id &&
|
|
29
|
+
currentLanguage === (getPrimaryLanguage(profile.data) ?? FALLBACK_LOCALE));
|
|
30
|
+
return (
|
|
31
|
+
<LanguageBridge config={language} accountReady={accountReady}>
|
|
32
|
+
{children}
|
|
33
|
+
</LanguageBridge>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -1,49 +1,53 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import { coerceToSupportedLocale } from '@oxy.so/core';
|
|
3
3
|
import { useOxy } from '../context/oxyRuntimeContext';
|
|
4
4
|
import type { OxyLanguageConfig } from '../types/navigation';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* `OxyProvider`'s language config prop, realized. Oxy decides which
|
|
8
|
-
* language an account (or, signed out, the device/guest locale) should see;
|
|
9
|
-
* this is the ONE place that decision reaches the host app's own i18n library
|
|
10
|
-
* — so an app never re-derives or imperatively tracks a language of its own,
|
|
11
|
-
* and never mounts anything extra to get it. It owns only its translation
|
|
12
|
-
* catalogs.
|
|
13
|
-
*
|
|
14
|
-
* `currentLanguage` may name a locale the host app never shipped a catalog
|
|
15
|
-
* for — {@link coerceToSupportedLocale} narrows it to the closest one the app
|
|
16
|
-
* actually has (exact match, then same base language, then `fallbackLocale`).
|
|
17
|
-
*
|
|
18
|
-
* Renders nothing. Mounted by `OxyProvider` INSIDE `OxyRuntimeProvider` only
|
|
19
|
-
* when this config is supplied, so an app with no i18n of its own pays
|
|
20
|
-
* nothing.
|
|
21
|
-
*/
|
|
6
|
+
/** The SDK resolves the entry/account locale before exposing translated app content. */
|
|
22
7
|
export function LanguageBridge({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const onErrorRef = useRef(onError);
|
|
40
|
-
onErrorRef.current = onError;
|
|
8
|
+
config,
|
|
9
|
+
accountReady = true,
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
config?: OxyLanguageConfig;
|
|
13
|
+
accountReady?: boolean;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}) {
|
|
16
|
+
const { currentLanguage, isLanguageReady, isAuthResolved } = useOxy();
|
|
17
|
+
const resolvedLocale = config
|
|
18
|
+
? coerceToSupportedLocale(currentLanguage, config.supportedLocales, config.fallbackLocale)
|
|
19
|
+
: currentLanguage;
|
|
20
|
+
const configRef = useRef(config);
|
|
21
|
+
configRef.current = config;
|
|
22
|
+
const pendingChange = useRef(Promise.resolve());
|
|
23
|
+
const [prepared, setPrepared] = useState(false);
|
|
41
24
|
|
|
42
25
|
useEffect(() => {
|
|
43
|
-
|
|
44
|
-
|
|
26
|
+
if (!isLanguageReady || !isAuthResolved || !accountReady) return;
|
|
27
|
+
let active = true;
|
|
28
|
+
// A slow earlier catalog must finish before a later locale is committed.
|
|
29
|
+
// Children stay mounted after boot: changing language never destroys route state.
|
|
30
|
+
pendingChange.current = pendingChange.current.then(async () => {
|
|
31
|
+
if (!active) return;
|
|
32
|
+
const current = configRef.current;
|
|
33
|
+
try {
|
|
34
|
+
await current?.onChange(resolvedLocale);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
current?.onError?.(error, resolvedLocale);
|
|
37
|
+
if (current && resolvedLocale !== current.fallbackLocale) {
|
|
38
|
+
try {
|
|
39
|
+
await current.onChange(current.fallbackLocale);
|
|
40
|
+
} catch (fallbackError) {
|
|
41
|
+
current.onError?.(fallbackError, current.fallbackLocale);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (active) setPrepared(true);
|
|
45
46
|
});
|
|
46
|
-
|
|
47
|
+
return () => {
|
|
48
|
+
active = false;
|
|
49
|
+
};
|
|
50
|
+
}, [accountReady, isAuthResolved, isLanguageReady, resolvedLocale]);
|
|
47
51
|
|
|
48
|
-
return null;
|
|
52
|
+
return !config || prepared ? children : null;
|
|
49
53
|
}
|
|
@@ -20,19 +20,10 @@ import { isNetConnectivityOnline } from '../utils/netConnectivity';
|
|
|
20
20
|
import { KeyboardBoundary } from './KeyboardBoundary';
|
|
21
21
|
import { SafeAreaRoot } from './SafeAreaRoot';
|
|
22
22
|
import { ProductAnalyticsObserver } from '../analytics/productAnalytics';
|
|
23
|
-
import {
|
|
23
|
+
import { AccountLanguageBoundary } from './AccountLanguageBoundary';
|
|
24
24
|
import { BloomLocaleBridge } from './BloomLocaleBridge';
|
|
25
|
-
import { useOxy } from '../context/oxyRuntimeContext';
|
|
26
|
-
import { useCurrentUser } from '../hooks/queries/useAccountQueries';
|
|
27
25
|
import { SupervisionStateObserver } from '../hooks/queries/useSupervision';
|
|
28
26
|
|
|
29
|
-
/** Hydrate owner preferences after the bearer is ready, including on cold boot. */
|
|
30
|
-
function AccountProfileObserver() {
|
|
31
|
-
const { canUsePrivateApi } = useOxy();
|
|
32
|
-
useCurrentUser({ enabled: canUsePrivateApi });
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
27
|
const bootStyles = StyleSheet.create({
|
|
37
28
|
providerRoot: {
|
|
38
29
|
flex: 1,
|
|
@@ -114,6 +105,7 @@ const OxyProvider: FC<OxyProviderProps> = ({
|
|
|
114
105
|
requireAuth = 'off',
|
|
115
106
|
backgroundSession = false,
|
|
116
107
|
language,
|
|
108
|
+
campaignAttribution = false,
|
|
117
109
|
}) => {
|
|
118
110
|
const support = useMemo(
|
|
119
111
|
() => ({ onAskAlia, documentationUrl, aliaAvatar, oxyOneArtwork }),
|
|
@@ -279,26 +271,28 @@ const OxyProvider: FC<OxyProviderProps> = ({
|
|
|
279
271
|
sessionMode={sessionMode}
|
|
280
272
|
webAuthMode={webAuthMode}
|
|
281
273
|
backgroundSession={backgroundSession}
|
|
274
|
+
legacyLanguageKeys={language?.legacyStorageKeys}
|
|
275
|
+
captureCampaignAttribution={campaignAttribution}
|
|
282
276
|
platformStorage={platformStorage}
|
|
283
277
|
accountQueries={accountQueries}
|
|
284
278
|
onAuthStateChange={onAuthStateChange as OxyRuntimeProviderProps['onAuthStateChange']}
|
|
285
279
|
>
|
|
286
|
-
<AccountProfileObserver />
|
|
287
280
|
<SupervisionStateObserver />
|
|
288
281
|
{productAnalytics ? <ProductAnalyticsObserver analytics={productAnalytics} /> : null}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
282
|
+
<AccountLanguageBoundary language={language}>
|
|
283
|
+
<BloomLocaleBridge>
|
|
284
|
+
<OxySupportContext.Provider value={support}>
|
|
285
|
+
<SurfaceProvider>
|
|
286
|
+
{requireAuth === 'off' ? (
|
|
287
|
+
children
|
|
288
|
+
) : (
|
|
289
|
+
<RequireOxyAuth prompt={requireAuth}>{children}</RequireOxyAuth>
|
|
290
|
+
)}
|
|
291
|
+
</SurfaceProvider>
|
|
292
|
+
</OxySupportContext.Provider>
|
|
293
|
+
<ToastOutlet />
|
|
294
|
+
</BloomLocaleBridge>
|
|
295
|
+
</AccountLanguageBoundary>
|
|
302
296
|
</OxyRuntimeProvider>
|
|
303
297
|
</QueryClientProvider>
|
|
304
298
|
);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, waitFor } from '@testing-library/react';
|
|
3
|
+
import { AccountLanguageBoundary } from '../AccountLanguageBoundary';
|
|
4
|
+
|
|
5
|
+
let mockCurrentLanguage = 'en-US';
|
|
6
|
+
let mockCanUsePrivateApi = true;
|
|
7
|
+
const mockUser = { id: 'account', publicKey: '', username: 'ada', name: {} };
|
|
8
|
+
let mockQuery: {
|
|
9
|
+
data?: typeof mockUser & { languages?: string[] };
|
|
10
|
+
dataUpdatedAt: number;
|
|
11
|
+
isError: boolean;
|
|
12
|
+
fetchStatus?: 'fetching' | 'paused' | 'idle';
|
|
13
|
+
};
|
|
14
|
+
jest.mock('../../context/oxyRuntimeContext', () => ({
|
|
15
|
+
useOxy: () => ({
|
|
16
|
+
canUsePrivateApi: mockCanUsePrivateApi,
|
|
17
|
+
user: mockUser,
|
|
18
|
+
currentLanguage: mockCurrentLanguage,
|
|
19
|
+
isLanguageReady: true,
|
|
20
|
+
isAuthResolved: true,
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
jest.mock('../../hooks/queries/useAccountQueries', () => ({ useCurrentUser: () => mockQuery }));
|
|
24
|
+
|
|
25
|
+
it('waits for the owner preference query and its runtime projection before painting the account app', async () => {
|
|
26
|
+
mockQuery = { data: mockUser, dataUpdatedAt: 0, isError: false };
|
|
27
|
+
const onChange = jest.fn();
|
|
28
|
+
const config = { supportedLocales: ['en-US', 'es-ES'], fallbackLocale: 'en-US', onChange };
|
|
29
|
+
const screen = render(
|
|
30
|
+
<AccountLanguageBoundary language={config}>
|
|
31
|
+
<span>App</span>
|
|
32
|
+
</AccountLanguageBoundary>,
|
|
33
|
+
);
|
|
34
|
+
expect(screen.queryByText('App')).toBeNull();
|
|
35
|
+
mockQuery = { data: { ...mockUser, languages: ['es-ES'] }, dataUpdatedAt: 123, isError: false };
|
|
36
|
+
screen.rerender(
|
|
37
|
+
<AccountLanguageBoundary language={config}>
|
|
38
|
+
<span>App</span>
|
|
39
|
+
</AccountLanguageBoundary>,
|
|
40
|
+
);
|
|
41
|
+
expect(screen.queryByText('App')).toBeNull();
|
|
42
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
43
|
+
mockCurrentLanguage = 'es-ES';
|
|
44
|
+
screen.rerender(
|
|
45
|
+
<AccountLanguageBoundary language={config}>
|
|
46
|
+
<span>App</span>
|
|
47
|
+
</AccountLanguageBoundary>,
|
|
48
|
+
);
|
|
49
|
+
await waitFor(() => expect(screen.getByText('App')).toBeTruthy());
|
|
50
|
+
expect(onChange.mock.calls).toEqual([['es-ES']]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.each(['paused', 'disabled', 'error'] as const)(
|
|
54
|
+
'renders the resolved fallback when the account query is %s, then adopts its preference without remounting',
|
|
55
|
+
async (unavailable) => {
|
|
56
|
+
mockCurrentLanguage = 'en-US';
|
|
57
|
+
mockCanUsePrivateApi = unavailable !== 'disabled';
|
|
58
|
+
mockQuery = {
|
|
59
|
+
data: mockUser,
|
|
60
|
+
dataUpdatedAt: 0,
|
|
61
|
+
isError: unavailable === 'error',
|
|
62
|
+
fetchStatus: unavailable === 'paused' ? 'paused' : 'idle',
|
|
63
|
+
};
|
|
64
|
+
const onChange = jest.fn();
|
|
65
|
+
const config = { supportedLocales: ['en-US', 'es-ES'], fallbackLocale: 'en-US', onChange };
|
|
66
|
+
const view = () => (
|
|
67
|
+
<AccountLanguageBoundary language={config}>
|
|
68
|
+
<input aria-label="Draft" defaultValue="Kept" />
|
|
69
|
+
</AccountLanguageBoundary>
|
|
70
|
+
);
|
|
71
|
+
const screen = render(view());
|
|
72
|
+
await waitFor(() => expect(screen.getByLabelText('Draft')).toBeTruthy());
|
|
73
|
+
const draft = screen.getByLabelText('Draft');
|
|
74
|
+
expect(onChange.mock.calls).toEqual([['en-US']]);
|
|
75
|
+
mockCanUsePrivateApi = true;
|
|
76
|
+
mockQuery = {
|
|
77
|
+
data: { ...mockUser, languages: ['es-ES'] },
|
|
78
|
+
dataUpdatedAt: 123,
|
|
79
|
+
isError: false,
|
|
80
|
+
fetchStatus: 'idle',
|
|
81
|
+
};
|
|
82
|
+
mockCurrentLanguage = 'es-ES';
|
|
83
|
+
screen.rerender(view());
|
|
84
|
+
await waitFor(() => expect(onChange.mock.calls).toEqual([['en-US'], ['es-ES']]));
|
|
85
|
+
expect(screen.getByLabelText('Draft')).toBe(draft);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
@@ -1,91 +1,111 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
2
|
+
import { act, render, waitFor } from '@testing-library/react';
|
|
3
3
|
import { LanguageBridge } from '../LanguageBridge';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* `OxyProvider`'s language config prop, realized: the one place Oxy's
|
|
7
|
-
* resolved language reaches a host app's own i18n library. `useOxy` is mocked here —
|
|
8
|
-
* it is the SDK's own well-tested context — so these tests exercise exactly
|
|
9
|
-
* what this component adds: coercion to the caller's catalog, and calling
|
|
10
|
-
* back only when the RESOLVED locale actually changes.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
5
|
let mockCurrentLanguage = 'en-US';
|
|
6
|
+
let mockLanguageReady = true;
|
|
7
|
+
let mockAuthResolved = true;
|
|
14
8
|
jest.mock('../../context/oxyRuntimeContext', () => ({
|
|
15
|
-
useOxy: () => ({
|
|
9
|
+
useOxy: () => ({
|
|
10
|
+
currentLanguage: mockCurrentLanguage,
|
|
11
|
+
isLanguageReady: mockLanguageReady,
|
|
12
|
+
isAuthResolved: mockAuthResolved,
|
|
13
|
+
}),
|
|
16
14
|
}));
|
|
17
|
-
|
|
18
15
|
const HOST_CATALOG = ['en-US', 'es-ES', 'fr-FR'] as const;
|
|
19
|
-
|
|
20
16
|
beforeEach(() => {
|
|
21
17
|
mockCurrentLanguage = 'en-US';
|
|
18
|
+
mockLanguageReady = true;
|
|
19
|
+
mockAuthResolved = true;
|
|
22
20
|
});
|
|
23
21
|
|
|
24
|
-
it(
|
|
25
|
-
|
|
22
|
+
it.each([
|
|
23
|
+
['es-ES', 'es-ES'],
|
|
24
|
+
['es-MX', 'es-ES'],
|
|
25
|
+
['ja-JP', 'en-US'],
|
|
26
|
+
])('resolves %s to the host catalog %s', async (locale, expected) => {
|
|
27
|
+
mockCurrentLanguage = locale;
|
|
26
28
|
const onChange = jest.fn();
|
|
27
29
|
render(
|
|
28
|
-
<LanguageBridge
|
|
30
|
+
<LanguageBridge config={{ supportedLocales: HOST_CATALOG, fallbackLocale: 'en-US', onChange }}>
|
|
31
|
+
<span>App</span>
|
|
32
|
+
</LanguageBridge>,
|
|
29
33
|
);
|
|
30
|
-
|
|
31
|
-
expect(onChange).toHaveBeenCalledWith('es-ES');
|
|
34
|
+
await waitFor(() => expect(onChange).toHaveBeenCalledWith(expected));
|
|
32
35
|
});
|
|
33
36
|
|
|
34
|
-
it('
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
it('does not paint any app text before saved preferences, account restoration and catalog loading finish', async () => {
|
|
38
|
+
mockLanguageReady = false;
|
|
39
|
+
mockAuthResolved = false;
|
|
40
|
+
let finishCatalog: (() => void) | undefined;
|
|
41
|
+
const onChange = jest.fn(
|
|
42
|
+
() =>
|
|
43
|
+
new Promise<void>((resolve) => {
|
|
44
|
+
finishCatalog = resolve;
|
|
45
|
+
}),
|
|
39
46
|
);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
expect(onChange).toHaveBeenCalledWith('
|
|
47
|
+
const config = { supportedLocales: HOST_CATALOG, fallbackLocale: 'en-US', onChange };
|
|
48
|
+
const children = <span>Translated app</span>;
|
|
49
|
+
const screen = render(<LanguageBridge config={config}>{children}</LanguageBridge>);
|
|
50
|
+
expect(screen.queryByText('Translated app')).toBeNull();
|
|
51
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
52
|
+
mockLanguageReady = true;
|
|
53
|
+
mockCurrentLanguage = 'es-ES';
|
|
54
|
+
screen.rerender(<LanguageBridge config={config}>{children}</LanguageBridge>);
|
|
55
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
56
|
+
mockAuthResolved = true;
|
|
57
|
+
screen.rerender(<LanguageBridge config={config}>{children}</LanguageBridge>);
|
|
58
|
+
await waitFor(() => expect(onChange).toHaveBeenCalledWith('es-ES'));
|
|
59
|
+
expect(screen.queryByText('Translated app')).toBeNull();
|
|
60
|
+
await act(async () => finishCatalog?.());
|
|
61
|
+
expect(screen.getByText('Translated app')).toBeTruthy();
|
|
52
62
|
});
|
|
53
63
|
|
|
54
|
-
it('
|
|
64
|
+
it('keeps route state mounted across a language change and ignores callback identity changes', async () => {
|
|
65
|
+
let mounts = 0;
|
|
66
|
+
function Page() {
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
mounts += 1;
|
|
69
|
+
}, []);
|
|
70
|
+
return <span>App</span>;
|
|
71
|
+
}
|
|
55
72
|
const onChange = jest.fn();
|
|
56
|
-
const {
|
|
57
|
-
|
|
73
|
+
const config = { supportedLocales: HOST_CATALOG, fallbackLocale: 'en-US', onChange };
|
|
74
|
+
const screen = render(
|
|
75
|
+
<LanguageBridge config={config}>
|
|
76
|
+
<Page />
|
|
77
|
+
</LanguageBridge>,
|
|
58
78
|
);
|
|
59
|
-
expect(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<LanguageBridge supportedLocales={HOST_CATALOG} fallbackLocale="en-US" onChange={() => {}} />,
|
|
79
|
+
await waitFor(() => expect(mounts).toBe(1));
|
|
80
|
+
screen.rerender(
|
|
81
|
+
<LanguageBridge config={{ ...config, onChange: jest.fn() }}>
|
|
82
|
+
<Page />
|
|
83
|
+
</LanguageBridge>,
|
|
65
84
|
);
|
|
66
85
|
expect(onChange).toHaveBeenCalledTimes(1);
|
|
67
|
-
|
|
68
86
|
mockCurrentLanguage = 'fr-FR';
|
|
69
|
-
rerender(
|
|
70
|
-
<LanguageBridge
|
|
87
|
+
screen.rerender(
|
|
88
|
+
<LanguageBridge config={config}>
|
|
89
|
+
<Page />
|
|
90
|
+
</LanguageBridge>,
|
|
71
91
|
);
|
|
72
|
-
expect(onChange).
|
|
73
|
-
expect(
|
|
92
|
+
await waitFor(() => expect(onChange).toHaveBeenLastCalledWith('fr-FR'));
|
|
93
|
+
expect(mounts).toBe(1);
|
|
74
94
|
});
|
|
75
95
|
|
|
76
|
-
it('reports a
|
|
77
|
-
|
|
78
|
-
const
|
|
96
|
+
it('reports a failed catalog and initializes the fallback before showing children', async () => {
|
|
97
|
+
mockCurrentLanguage = 'es-ES';
|
|
98
|
+
const failure = new Error('catalog unavailable');
|
|
99
|
+
const onChange = jest.fn().mockRejectedValueOnce(failure).mockResolvedValueOnce(undefined);
|
|
79
100
|
const onError = jest.fn();
|
|
80
|
-
render(
|
|
101
|
+
const screen = render(
|
|
81
102
|
<LanguageBridge
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/>,
|
|
103
|
+
config={{ supportedLocales: HOST_CATALOG, fallbackLocale: 'en-US', onChange, onError }}
|
|
104
|
+
>
|
|
105
|
+
<span>App</span>
|
|
106
|
+
</LanguageBridge>,
|
|
87
107
|
);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
expect(
|
|
108
|
+
await waitFor(() => expect(screen.getByText('App')).toBeTruthy());
|
|
109
|
+
expect(onError).toHaveBeenCalledWith(failure, 'es-ES');
|
|
110
|
+
expect(onChange.mock.calls.map(([locale]) => locale)).toEqual(['es-ES', 'en-US']);
|
|
91
111
|
});
|
|
@@ -67,7 +67,13 @@ export const OxySignUpPanel: React.FC<OxySignUpPanelProps> = ({
|
|
|
67
67
|
host = 'page',
|
|
68
68
|
}) => {
|
|
69
69
|
const { t } = useI18n();
|
|
70
|
-
const {
|
|
70
|
+
const {
|
|
71
|
+
oxyServices,
|
|
72
|
+
handleWebSession,
|
|
73
|
+
accountDialogController,
|
|
74
|
+
currentLanguage,
|
|
75
|
+
campaignAttribution,
|
|
76
|
+
} = useOxy();
|
|
71
77
|
const snapshot = useAccountDialogSnapshot(accountDialogController);
|
|
72
78
|
const flowOwner = host === 'dialog' ? accountDialogController : null;
|
|
73
79
|
const [saved] = useState(() => readSignInFlow<SavedSignUpFlow>(flowOwner, SIGN_UP_FLOW_KEY));
|
|
@@ -139,6 +145,8 @@ export const OxySignUpPanel: React.FC<OxySignUpPanelProps> = ({
|
|
|
139
145
|
username: username.trim(),
|
|
140
146
|
email,
|
|
141
147
|
emailTicket: ticket,
|
|
148
|
+
languages: [currentLanguage],
|
|
149
|
+
...(campaignAttribution ? { campaignAttribution } : {}),
|
|
142
150
|
});
|
|
143
151
|
await handleWebSession(session);
|
|
144
152
|
writeSignInFlow(flowOwner, SIGN_UP_FLOW_KEY, undefined);
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
import { createTokenLossRecovery, TOKEN_RECOVERY_BACKOFF_MS } from '../session/tokenLossRecovery';
|
|
63
63
|
import { createCachedBootStore, type CachedBootStore } from '../session/cachedBoot';
|
|
64
64
|
import { bindAuthStoreToRuntime } from '../stores/authStore';
|
|
65
|
+
import { useEntryLink } from '../hooks/useEntryLink';
|
|
65
66
|
import { useLanguageManagement } from '../hooks/useLanguageManagement';
|
|
66
67
|
import { useSessionManagement } from '../hooks/useSessionManagement';
|
|
67
68
|
import { useAuthOperations, clearPersistedAuthSafe } from './hooks/useAuthOperations';
|
|
@@ -153,6 +154,8 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
153
154
|
sessionMode = 'account',
|
|
154
155
|
webAuthMode = 'popup',
|
|
155
156
|
backgroundSession = false,
|
|
157
|
+
legacyLanguageKeys,
|
|
158
|
+
captureCampaignAttribution = false,
|
|
156
159
|
platformStorage,
|
|
157
160
|
accountQueries,
|
|
158
161
|
onAuthStateChange,
|
|
@@ -415,8 +418,10 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
415
418
|
.catch((err) => {
|
|
416
419
|
// Nothing persisted can be restored; the account cache still has to
|
|
417
420
|
// become ready, or RequireOxyAuth would wait forever.
|
|
418
|
-
|
|
421
|
+
const memoryStorage = createMemoryStorage();
|
|
422
|
+
accountQueryPersistence.setStorage(memoryStorage);
|
|
419
423
|
if (mounted) {
|
|
424
|
+
setStorage(memoryStorage);
|
|
420
425
|
logger('Failed to initialize storage', err);
|
|
421
426
|
onError?.({
|
|
422
427
|
message: 'Failed to initialize storage',
|
|
@@ -430,8 +435,15 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
430
435
|
};
|
|
431
436
|
}, [accountQueryPersistence, logger, onError, platformStorage, runtime, storageReady]);
|
|
432
437
|
|
|
438
|
+
const entryLink = useEntryLink({
|
|
439
|
+
storage,
|
|
440
|
+
storageKey: `${storageKeyPrefix}_campaign_attribution`,
|
|
441
|
+
captureCampaign: captureCampaignAttribution,
|
|
442
|
+
onError: logger,
|
|
443
|
+
});
|
|
433
444
|
const {
|
|
434
445
|
currentLanguage,
|
|
446
|
+
isLanguageReady,
|
|
435
447
|
languages: currentLanguages,
|
|
436
448
|
metadata: currentLanguageMetadata,
|
|
437
449
|
languageName: currentLanguageName,
|
|
@@ -441,6 +453,9 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
441
453
|
} = useLanguageManagement({
|
|
442
454
|
storage,
|
|
443
455
|
languageKey: storageKeys.language,
|
|
456
|
+
entryUrl: entryLink.url,
|
|
457
|
+
entryLinkReady: entryLink.ready,
|
|
458
|
+
legacyLanguageKeys,
|
|
444
459
|
user,
|
|
445
460
|
onError,
|
|
446
461
|
logger,
|
|
@@ -1751,6 +1766,8 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
1751
1766
|
webAuthMode,
|
|
1752
1767
|
error,
|
|
1753
1768
|
currentLanguage,
|
|
1769
|
+
isLanguageReady: isLanguageReady && entryLink.campaignReady,
|
|
1770
|
+
campaignAttribution: entryLink.campaignAttribution,
|
|
1754
1771
|
currentLanguages,
|
|
1755
1772
|
currentLanguageMetadata,
|
|
1756
1773
|
currentLanguageName,
|
|
@@ -1808,6 +1825,9 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
|
|
|
1808
1825
|
webAuthMode,
|
|
1809
1826
|
error,
|
|
1810
1827
|
currentLanguage,
|
|
1828
|
+
isLanguageReady,
|
|
1829
|
+
entryLink.campaignReady,
|
|
1830
|
+
entryLink.campaignAttribution,
|
|
1811
1831
|
currentLanguages,
|
|
1812
1832
|
currentLanguageMetadata,
|
|
1813
1833
|
currentLanguageName,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CampaignAttribution } from '@oxy.so/contracts';
|
|
1
2
|
import type {
|
|
2
3
|
StartNativeOAuthSignInOptions,
|
|
3
4
|
NativeOAuthSignInResult,
|
|
@@ -85,6 +86,8 @@ export interface OxyContextState {
|
|
|
85
86
|
error: string | null;
|
|
86
87
|
/** Active UI locale (`language-REGION`): the account's primary locale when signed in, else the guest/device locale. */
|
|
87
88
|
currentLanguage: string;
|
|
89
|
+
isLanguageReady: boolean;
|
|
90
|
+
campaignAttribution?: CampaignAttribution;
|
|
88
91
|
/** Ordered account locales (primary first) when signed in, or the single guest override when signed out. */
|
|
89
92
|
currentLanguages: string[];
|
|
90
93
|
currentLanguageMetadata: ReturnType<typeof useLanguageManagement>['metadata'];
|
|
@@ -180,6 +183,8 @@ export interface OxyContextState {
|
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
export interface OxyRuntimeProviderProps {
|
|
186
|
+
legacyLanguageKeys?: readonly string[];
|
|
187
|
+
captureCampaignAttribution?: boolean;
|
|
183
188
|
children: ReactNode;
|
|
184
189
|
oxyServices?: OxyServices;
|
|
185
190
|
baseURL?: string;
|