@lowdefy/shell 0.0.0-experimental-20261019052246
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/LICENSE +201 -0
- package/README.md +308 -0
- package/app/(t0,t1,t2,t3,t4)/[...path].js +38 -0
- package/app/(t0,t1,t2,t3,t4)/_layout.js +48 -0
- package/app/(t0,t1,t2,t3,t4)/index.js +38 -0
- package/app/+native-intent.js +30 -0
- package/app/_layout.js +33 -0
- package/app.config.ts +168 -0
- package/applyShellPlugins.js +43 -0
- package/authCallbackIntentFilters.js +46 -0
- package/babel.config.js +25 -0
- package/checkShellConfig.js +145 -0
- package/checkShellPlugins.js +74 -0
- package/getPrivacyManifest.js +39 -0
- package/metro.config.js +73 -0
- package/native-fingerprint.json +12 -0
- package/package.json +130 -0
- package/profiles.js +274 -0
- package/readShellConfig.js +36 -0
- package/resolveShellPlugins.js +109 -0
- package/runtimeVersion.js +105 -0
- package/scripts/checkReleaseBundle.mjs +358 -0
- package/scripts/createDayjsLocales.mjs +67 -0
- package/scripts/createShellAssets.mjs +54 -0
- package/scripts/createShellVersion.mjs +83 -0
- package/scripts/createTypeManifest.mjs +205 -0
- package/shell.dev.json +11 -0
- package/shellVariant.js +111 -0
- package/src/ShellContext.js +32 -0
- package/src/actions/CopyToClipboard.js +29 -0
- package/src/actions/CopyToClipboard.test.js +33 -0
- package/src/actions/GeolocationCurrentPosition.js +55 -0
- package/src/actions/GeolocationCurrentPosition.test.js +66 -0
- package/src/actions/GetLocalStorage.js +36 -0
- package/src/actions/RemoveLocalStorage.js +31 -0
- package/src/actions/ScrollTo.js +66 -0
- package/src/actions/ScrollTo.test.js +83 -0
- package/src/actions/SetFocus.js +33 -0
- package/src/actions/SetFocus.test.js +49 -0
- package/src/actions/SetLocalStorage.js +34 -0
- package/src/actions/localStorage.test.js +68 -0
- package/src/actions/measureInScrollView.js +36 -0
- package/src/actions/nativeActions.js +41 -0
- package/src/auth/AuthButton.js +44 -0
- package/src/auth/AuthGate.js +57 -0
- package/src/auth/AuthLink.js +43 -0
- package/src/auth/LockScreen.js +67 -0
- package/src/auth/LockScreen.test.js +37 -0
- package/src/auth/SignInScreen.js +256 -0
- package/src/auth/SignInScreen.test.js +182 -0
- package/src/auth/createAppAuth.js +306 -0
- package/src/auth/createAppAuth.test.js +414 -0
- package/src/auth/createAuthApi.js +99 -0
- package/src/auth/createAuthApi.test.js +135 -0
- package/src/auth/createCoreAuth.js +96 -0
- package/src/auth/createCoreAuth.test.js +188 -0
- package/src/auth/createSessionStores.js +45 -0
- package/src/auth/createTokenStore.js +103 -0
- package/src/auth/createTokenStore.test.js +116 -0
- package/src/auth/getAuthSettings.js +24 -0
- package/src/auth/getAuthView.js +39 -0
- package/src/auth/getAuthView.test.js +52 -0
- package/src/auth/getSheetReturnUrl.js +35 -0
- package/src/auth/getTokenStoreOptions.js +46 -0
- package/src/auth/isBackgroundShell.js +26 -0
- package/src/auth/isSheetReturnLink.js +35 -0
- package/src/auth/parseQuery.js +39 -0
- package/src/auth/purgeAppData.js +45 -0
- package/src/auth/shouldLock.js +26 -0
- package/src/auth/signInWithSheet.js +97 -0
- package/src/auth/signInWithSheet.test.js +205 -0
- package/src/auth/styles.js +78 -0
- package/src/auth/unregisterPushToken.js +21 -0
- package/src/auth/usesBiometricUnlock.js +23 -0
- package/src/auth/watchSession.js +47 -0
- package/src/auth/withoutCookies.js +26 -0
- package/src/cache/createSqliteCacheStore.js +113 -0
- package/src/cache/createSqliteCacheStore.test.js +93 -0
- package/src/cache/getCacheKey.js +25 -0
- package/src/cache/getLastCacheKeyName.js +22 -0
- package/src/components/AppProvider.js +90 -0
- package/src/components/Frame.js +116 -0
- package/src/components/Frame.test.js +101 -0
- package/src/components/LoadingScreen.js +35 -0
- package/src/components/MenuButton.js +108 -0
- package/src/components/MenuButton.test.js +51 -0
- package/src/components/MessageScreen.js +51 -0
- package/src/components/NavigationTheme.js +33 -0
- package/src/components/NoConnectionBanner.js +51 -0
- package/src/components/NoConnectionBanner.test.js +73 -0
- package/src/components/OtherPageScreen.js +56 -0
- package/src/components/PageScreen.js +244 -0
- package/src/components/PageScreen.test.js +492 -0
- package/src/components/ShellRoot.js +34 -0
- package/src/components/ShellRoot.test.js +62 -0
- package/src/components/ShellThemeProvider.js +45 -0
- package/src/components/SwitchToTab.js +36 -0
- package/src/components/SwitchToTab.test.js +63 -0
- package/src/components/TabIconImages.js +72 -0
- package/src/components/UpdateScreen.js +36 -0
- package/src/components/getNavigationTheme.js +44 -0
- package/src/components/getNavigationTheme.test.js +41 -0
- package/src/components/styles.js +50 -0
- package/src/components/useSurfaceTokens.js +47 -0
- package/src/config/getAppApiBase.js +31 -0
- package/src/config/getShellConfig.js +35 -0
- package/src/config/getShellConfig.test.js +80 -0
- package/src/createShellApp.js +105 -0
- package/src/dev/DevShell.js +228 -0
- package/src/dev/DevShell.test.js +233 -0
- package/src/dev/connect/ConfirmSheet.js +71 -0
- package/src/dev/connect/ConnectScreen.js +117 -0
- package/src/dev/connect/QrScanner.js +89 -0
- package/src/dev/connection/createDevShellConfig.js +47 -0
- package/src/dev/connection/createDevTransport.js +51 -0
- package/src/dev/connection/createDevTransport.test.js +87 -0
- package/src/dev/connection/createServerList.js +48 -0
- package/src/dev/connection/isLoopbackHost.js +25 -0
- package/src/dev/connection/pairedShellApp.test.js +104 -0
- package/src/dev/connection/parseDevLink.js +66 -0
- package/src/dev/connection/parseDevLink.test.js +97 -0
- package/src/dev/connection/parseQuery.js +33 -0
- package/src/dev/connection/parseServerInput.js +40 -0
- package/src/dev/connection/parseServerUrl.js +45 -0
- package/src/dev/devStyles.js +97 -0
- package/src/dev/index.js +21 -0
- package/src/dev/inject/createDeviceInjections.js +185 -0
- package/src/dev/inject/createDeviceInjections.test.js +208 -0
- package/src/dev/inject/createOfflineFetch.js +35 -0
- package/src/dev/inject/createOfflineFetch.test.js +68 -0
- package/src/dev/menu/BlockGallery.js +33 -0
- package/src/dev/menu/DevMenu.js +121 -0
- package/src/dev/menu/createLogBuffer.js +83 -0
- package/src/dev/menu/watchShake.js +52 -0
- package/src/dev/menu/watchShake.test.js +61 -0
- package/src/dev/recorder/createNativeRecorder.js +204 -0
- package/src/dev/recorder/createNativeRecorder.test.js +307 -0
- package/src/dev/redirectDevLink.js +30 -0
- package/src/dev/redirectDevLink.test.js +49 -0
- package/src/dev/steps/createStepRunner.js +184 -0
- package/src/dev/steps/createStepRunner.test.js +262 -0
- package/src/dev/steps/findBlock.js +78 -0
- package/src/dev/steps/isContextSettled.js +37 -0
- package/src/dev/steps/waitForSettle.js +39 -0
- package/src/dev/steps/waitUntil.js +40 -0
- package/src/dev/tab/DevTabClient.js +97 -0
- package/src/dev/tab/answerEval.js +39 -0
- package/src/dev/tab/answerInspect.js +45 -0
- package/src/dev/tab/answerLoadState.js +50 -0
- package/src/dev/tab/createDevTabClient.js +277 -0
- package/src/dev/tab/createDevTabClient.test.js +314 -0
- package/src/dev/tab/devClientMarker.js +22 -0
- package/src/dev/tab/devClientMarker.test.js +23 -0
- package/src/dev/tab/findPageContext.js +43 -0
- package/src/dev/tab/getShownPage.js +42 -0
- package/src/dev/tab/readEventStream.js +68 -0
- package/src/dev/tab/reloadApp.js +42 -0
- package/src/dev/tab/reloadApp.test.js +115 -0
- package/src/device/injected.js +60 -0
- package/src/device/injected.test.js +39 -0
- package/src/host/createDeviceStore.js +32 -0
- package/src/host/createMemoryStore.js +32 -0
- package/src/host/createNetworkStatus.js +94 -0
- package/src/host/createNetworkStatus.test.js +94 -0
- package/src/host/createRouterAdapter.js +33 -0
- package/src/host/createShellFetch.js +60 -0
- package/src/host/createShellFetch.test.js +150 -0
- package/src/host/createShellGlobals.js +62 -0
- package/src/host/createShellHost.js +73 -0
- package/src/host/createShellHost.test.js +36 -0
- package/src/host/getOrigin.js +26 -0
- package/src/host/isOnline.js +22 -0
- package/src/host/networkStatus.js +24 -0
- package/src/host/withShellHeader.js +25 -0
- package/src/host/withShellHeader.test.js +28 -0
- package/src/js/evaluateJsEntries.js +43 -0
- package/src/js/evaluateJsEntries.test.js +48 -0
- package/src/locale/createShellLocale.js +112 -0
- package/src/locale/createShellLocale.test.js +74 -0
- package/src/locale/dayjsLocales.js +165 -0
- package/src/locale/getDeviceLocales.js +23 -0
- package/src/locale/loadDayjsLocale.js +32 -0
- package/src/locale/resolveActiveLocale.js +46 -0
- package/src/locale/resolveActiveLocale.test.js +40 -0
- package/src/navigation/SlotContext.js +24 -0
- package/src/navigation/frameSlots.js +27 -0
- package/src/navigation/getFrame.js +30 -0
- package/src/navigation/getInitialScreenOptions.js +63 -0
- package/src/navigation/getInitialScreenOptions.test.js +66 -0
- package/src/navigation/getPresentation.js +28 -0
- package/src/navigation/getSlotRootPath.js +27 -0
- package/src/navigation/getStackOptions.js +185 -0
- package/src/navigation/getTabHref.js +45 -0
- package/src/navigation/getTabIcons.js +43 -0
- package/src/navigation/isPageUnderHeader.js +46 -0
- package/src/navigation/navigation.test.js +342 -0
- package/src/navigation/toRouterHref.js +29 -0
- package/src/plugins/appTypes.js +28 -0
- package/src/plugins/prefixTypes.js +28 -0
- package/src/plugins/shellPlugins.json +14 -0
- package/src/plugins/typeManifest.js +649 -0
- package/src/plugins/types.js +61 -0
- package/src/plugins/types.test.js +173 -0
- package/src/polyfills/installGetRandomValues.js +28 -0
- package/src/session/createAppSession.js +318 -0
- package/src/session/createAppSession.test.js +359 -0
- package/src/session/getPageView.js +59 -0
- package/src/session/getPageView.test.js +137 -0
- package/src/sync/encodePagePath.js +27 -0
- package/src/sync/fetchPage.js +49 -0
- package/src/sync/fetchRoot.js +38 -0
- package/src/sync/fetchUser.js +28 -0
- package/src/sync/runLimited.js +36 -0
- package/src/sync/toRootConfig.js +35 -0
- package/src/test/createFakeNetwork.js +40 -0
- package/src/test/createFakeSecureStore.js +40 -0
- package/src/test/createFakeServer.js +98 -0
- package/src/test/createMemoryCacheStore.js +59 -0
- package/src/test/expoImage.js +38 -0
- package/src/test/expoSpeechRecognition.js +31 -0
- package/src/test/expoUi.js +55 -0
- package/src/test/expoUiBottomSheet.js +64 -0
- package/src/test/expoUiDateTimePicker.js +26 -0
- package/src/test/expoUiMenu.js +46 -0
- package/src/test/expoUiSegmentedControl.js +47 -0
- package/src/test/expoUiSwiftUi.js +62 -0
- package/src/test/expoUiSwiftUiModifiers.js +23 -0
- package/src/test/expoWebBrowser.js +23 -0
- package/src/test/fixtures.js +98 -0
- package/src/test/renderPageScreen.js +109 -0
- package/src/types/checkTypeSet.js +39 -0
- package/src/types/collectPageTypes.js +49 -0
- package/src/types/collectPageTypes.test.js +90 -0
- package/src/types/findMissingPageTypes.js +40 -0
- package/src/version/compareVersions.js +38 -0
- package/src/version/isShellTooOld.js +30 -0
- package/src/version/shellVersion.js +21 -0
- package/src/version/shellVersion.test.js +32 -0
- package/withExcludedNativeModules.js +93 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { getAuthorizationHeaders } from '@lowdefy/client-core';
|
|
18
|
+
import { type } from '@lowdefy/helpers';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The auth client the core's auth actions call (client-core createAuthMethods), as the web's is
|
|
22
|
+
* its Better Auth client: each method calls a Better Auth route with the session token as a bearer
|
|
23
|
+
* token and resolves with Better Auth's `{ data, error }`. Calls go through the watched fetch, so
|
|
24
|
+
* a 401 ends the session on the phone as any other call's does.
|
|
25
|
+
*
|
|
26
|
+
* The session the shell holds (the caller, its roles and organisation, and the pages it may open)
|
|
27
|
+
* is the app session's last sync. A switch of organisation syncs it before the action resolves, so
|
|
28
|
+
* the switch takes effect at once; UpdateSession syncs it too.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} params
|
|
31
|
+
* @param {string} params.apiBase The app server's URL.
|
|
32
|
+
* @param {Function} params.fetch The watched fetch (watchSession).
|
|
33
|
+
* @param {Function} params.getSession The app session (createAppSession), once started.
|
|
34
|
+
* @param {{ get }} params.tokenStore The session token store.
|
|
35
|
+
*/
|
|
36
|
+
function createCoreAuth({ apiBase, fetch, getSession, tokenStore }) {
|
|
37
|
+
async function call({ body = {}, method = 'POST', path }) {
|
|
38
|
+
const headers = getAuthorizationHeaders({ host: { authTransport: 'bearer', tokenStore } });
|
|
39
|
+
const init = { method, headers };
|
|
40
|
+
if (method !== 'GET') {
|
|
41
|
+
init.headers = { ...headers, 'Content-Type': 'application/json' };
|
|
42
|
+
init.body = JSON.stringify(body);
|
|
43
|
+
}
|
|
44
|
+
const res = await fetch(`${apiBase}/api/auth${path}`, init);
|
|
45
|
+
// A route an app's org policy disables answers 404 with no JSON body.
|
|
46
|
+
const data = await res.json().catch(() => null);
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const details = type.isObject(data) ? data : {};
|
|
49
|
+
return { data: null, error: { ...details, status: res.status, statusText: res.statusText } };
|
|
50
|
+
}
|
|
51
|
+
return { data, error: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function sync() {
|
|
55
|
+
await getSession().sync();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function listOrganizations() {
|
|
59
|
+
return call({ method: 'GET', path: '/organization/list' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function setActiveOrganization({ organizationId, organizationSlug }) {
|
|
63
|
+
const result = await call({
|
|
64
|
+
path: '/organization/set-active',
|
|
65
|
+
body: { organizationId, organizationSlug },
|
|
66
|
+
});
|
|
67
|
+
if (result.error === null) {
|
|
68
|
+
await sync();
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function refreshSession() {
|
|
74
|
+
return call({ method: 'GET', path: '/get-session' });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getResolvedUser() {
|
|
78
|
+
await sync();
|
|
79
|
+
return { user: getSession().getState().user };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function signOut() {
|
|
83
|
+
return call({ path: '/sign-out' });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
authConfig: {},
|
|
88
|
+
getResolvedUser,
|
|
89
|
+
listOrganizations,
|
|
90
|
+
refreshSession,
|
|
91
|
+
setActiveOrganization,
|
|
92
|
+
signOut,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default createCoreAuth;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import createAppSession from '../session/createAppSession.js';
|
|
18
|
+
import createCoreAuth from './createCoreAuth.js';
|
|
19
|
+
import createFakeServer, { header } from '../test/createFakeServer.js';
|
|
20
|
+
import createMemoryCacheStore from '../test/createMemoryCacheStore.js';
|
|
21
|
+
import createMemoryStore from '../host/createMemoryStore.js';
|
|
22
|
+
import { createPages, createRoot } from '../test/fixtures.js';
|
|
23
|
+
|
|
24
|
+
const apiBase = 'https://field.example.com';
|
|
25
|
+
const token = 'session-token.signature';
|
|
26
|
+
|
|
27
|
+
const organizations = [
|
|
28
|
+
{ id: 'org-1', name: 'Acme', slug: 'acme' },
|
|
29
|
+
{ id: 'org-2', name: 'Globex', slug: 'globex' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// The app's server: Better Auth's organization routes over the bearer token, with the caller
|
|
33
|
+
// and user /api/user resolves moving to the organisation set-active makes active.
|
|
34
|
+
function authRoutes(server) {
|
|
35
|
+
return function answer({ path, init }) {
|
|
36
|
+
if (header(init, 'authorization') !== `Bearer ${token}`) {
|
|
37
|
+
return { status: 401, body: { code: 'UNAUTHORIZED', message: 'Unauthorized' } };
|
|
38
|
+
}
|
|
39
|
+
if (path === '/organization/list') {
|
|
40
|
+
return { body: organizations };
|
|
41
|
+
}
|
|
42
|
+
if (path === '/organization/set-active') {
|
|
43
|
+
const { organizationId } = JSON.parse(init.body);
|
|
44
|
+
if (!organizations.some((org) => org.id === organizationId)) {
|
|
45
|
+
return {
|
|
46
|
+
status: 403,
|
|
47
|
+
body: { code: 'USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION', message: 'Not a member.' },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
server.state.caller = `c-ana-${organizationId}; roles=r-${organizationId}`;
|
|
51
|
+
server.state.user = { id: 'ana', organization_id: organizationId, roles: ['editor'] };
|
|
52
|
+
return { body: organizations.find((org) => org.id === organizationId) };
|
|
53
|
+
}
|
|
54
|
+
if (path === '/get-session') {
|
|
55
|
+
return { body: { session: { id: 's1' }, user: { id: 'ana' } } };
|
|
56
|
+
}
|
|
57
|
+
if (path === '/sign-out') {
|
|
58
|
+
return { body: { success: true } };
|
|
59
|
+
}
|
|
60
|
+
return { status: 404, body: null };
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function setup({ heldToken = token } = {}) {
|
|
65
|
+
const server = createFakeServer({
|
|
66
|
+
caller: 'c-ana-org-1; roles=r-org-1',
|
|
67
|
+
user: { id: 'ana', organization_id: 'org-1', roles: ['viewer'] },
|
|
68
|
+
root: createRoot(),
|
|
69
|
+
rootEtag: '"root-1"',
|
|
70
|
+
pages: createPages([['today', 'today', '"p-today-1"']]),
|
|
71
|
+
});
|
|
72
|
+
server.state.auth = authRoutes(server);
|
|
73
|
+
const tokenStore = { get: () => heldToken };
|
|
74
|
+
const session = createAppSession({
|
|
75
|
+
apiBase,
|
|
76
|
+
app: { id: 'field' },
|
|
77
|
+
cacheStore: createMemoryCacheStore(),
|
|
78
|
+
fetch: server.fetch,
|
|
79
|
+
kv: createMemoryStore(),
|
|
80
|
+
tokenStore,
|
|
81
|
+
});
|
|
82
|
+
await session.start();
|
|
83
|
+
const coreAuth = createCoreAuth({
|
|
84
|
+
apiBase,
|
|
85
|
+
fetch: server.fetch,
|
|
86
|
+
getSession: () => session,
|
|
87
|
+
tokenStore,
|
|
88
|
+
});
|
|
89
|
+
return { coreAuth, server, session };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function authCalls(server) {
|
|
93
|
+
return server.fetch.mock.calls
|
|
94
|
+
.filter(([url]) => url.startsWith(`${apiBase}/api/auth/`))
|
|
95
|
+
.map(([url, init]) => ({
|
|
96
|
+
path: url.slice(`${apiBase}/api/auth`.length),
|
|
97
|
+
method: init.method,
|
|
98
|
+
authorization: header(init, 'authorization'),
|
|
99
|
+
body: init.body === undefined ? undefined : JSON.parse(init.body),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test('listOrganizations lists the memberships with the session token as a bearer', async () => {
|
|
104
|
+
const { coreAuth, server } = await setup();
|
|
105
|
+
expect(await coreAuth.listOrganizations()).toEqual({ data: organizations, error: null });
|
|
106
|
+
expect(authCalls(server)).toEqual([
|
|
107
|
+
{
|
|
108
|
+
path: '/organization/list',
|
|
109
|
+
method: 'GET',
|
|
110
|
+
authorization: `Bearer ${token}`,
|
|
111
|
+
body: undefined,
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('setActiveOrganization switches the organisation and syncs the session the shell holds', async () => {
|
|
117
|
+
const { coreAuth, server, session } = await setup();
|
|
118
|
+
const { generation, homeRequests } = session.getState();
|
|
119
|
+
expect(await coreAuth.setActiveOrganization({ organizationId: 'org-2' })).toEqual({
|
|
120
|
+
data: organizations[1],
|
|
121
|
+
error: null,
|
|
122
|
+
});
|
|
123
|
+
expect(authCalls(server)).toEqual([
|
|
124
|
+
{
|
|
125
|
+
path: '/organization/set-active',
|
|
126
|
+
method: 'POST',
|
|
127
|
+
authorization: `Bearer ${token}`,
|
|
128
|
+
body: { organizationId: 'org-2' },
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
const state = session.getState();
|
|
132
|
+
expect(state.user).toEqual({ id: 'ana', organization_id: 'org-2', roles: ['editor'] });
|
|
133
|
+
// Another caller: every screen is rebuilt for the new organisation, from its home page.
|
|
134
|
+
expect(state.generation).toBe(generation + 1);
|
|
135
|
+
expect(state.homeRequests).toBe(homeRequests + 1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('setActiveOrganization gives a refused switch as an error and leaves the session', async () => {
|
|
139
|
+
const { coreAuth, server, session } = await setup();
|
|
140
|
+
const before = session.getState();
|
|
141
|
+
const syncs = server.fetch.mock.calls.length;
|
|
142
|
+
expect(await coreAuth.setActiveOrganization({ organizationId: 'org-9' })).toEqual({
|
|
143
|
+
data: null,
|
|
144
|
+
error: {
|
|
145
|
+
code: 'USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION',
|
|
146
|
+
message: 'Not a member.',
|
|
147
|
+
status: 403,
|
|
148
|
+
statusText: undefined,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
expect(server.fetch.mock.calls).toHaveLength(syncs + 1);
|
|
152
|
+
expect(session.getState()).toBe(before);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('an organisation route an app disables answers as an error with its status', async () => {
|
|
156
|
+
const { coreAuth, server } = await setup();
|
|
157
|
+
server.state.auth = () => ({ status: 404, body: null });
|
|
158
|
+
expect(await coreAuth.listOrganizations()).toEqual({
|
|
159
|
+
data: null,
|
|
160
|
+
error: { status: 404, statusText: undefined },
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('refreshSession reads the session and getResolvedUser syncs the caller', async () => {
|
|
165
|
+
const { coreAuth, server } = await setup();
|
|
166
|
+
expect(await coreAuth.refreshSession()).toEqual({
|
|
167
|
+
data: { session: { id: 's1' }, user: { id: 'ana' } },
|
|
168
|
+
error: null,
|
|
169
|
+
});
|
|
170
|
+
server.state.user = { id: 'ana', organization_id: 'org-1', roles: ['admin'] };
|
|
171
|
+
expect(await coreAuth.getResolvedUser()).toEqual({
|
|
172
|
+
user: { id: 'ana', organization_id: 'org-1', roles: ['admin'] },
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('signOut ends the session on the server with the bearer token', async () => {
|
|
177
|
+
const { coreAuth, server } = await setup();
|
|
178
|
+
expect(await coreAuth.signOut()).toEqual({ data: { success: true }, error: null });
|
|
179
|
+
expect(authCalls(server)).toEqual([
|
|
180
|
+
{ path: '/sign-out', method: 'POST', authorization: `Bearer ${token}`, body: {} },
|
|
181
|
+
]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('a call with no session token held sends no authorization', async () => {
|
|
185
|
+
const { coreAuth, server } = await setup({ heldToken: null });
|
|
186
|
+
expect((await coreAuth.listOrganizations()).error.status).toBe(401);
|
|
187
|
+
expect(authCalls(server)[0].authorization).toBe(null);
|
|
188
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The page cache and device store the app's session writes through. Once the session has ended
|
|
19
|
+
* (isEnded), a sync or page load still in flight writes nothing: the person's pages arriving after
|
|
20
|
+
* their sign-out are dropped, not cached.
|
|
21
|
+
*/
|
|
22
|
+
function createSessionStores({ cacheStore, isEnded, kv }) {
|
|
23
|
+
function unlessEnded(write) {
|
|
24
|
+
return async (params) => {
|
|
25
|
+
if (isEnded()) return;
|
|
26
|
+
await write(params);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
cacheStore: {
|
|
31
|
+
read: async (cacheKey) => (isEnded() ? null : cacheStore.read(cacheKey)),
|
|
32
|
+
writeRoot: unlessEnded(cacheStore.writeRoot),
|
|
33
|
+
writePage: unlessEnded(cacheStore.writePage),
|
|
34
|
+
removePages: unlessEnded(cacheStore.removePages),
|
|
35
|
+
},
|
|
36
|
+
kv: {
|
|
37
|
+
get: (key) => kv.get(key),
|
|
38
|
+
set: (key, value) => {
|
|
39
|
+
if (!isEnded()) kv.set(key, value);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default createSessionStores;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as SecureStore from 'expo-secure-store';
|
|
18
|
+
|
|
19
|
+
import getTokenStoreOptions from './getTokenStoreOptions.js';
|
|
20
|
+
|
|
21
|
+
const PROTECTED = 'protected';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The session token store a bearer host gives the core (validateAuthTransport): one token per app
|
|
25
|
+
* in the Keychain or Keystore (getTokenStoreOptions), read once per unlock and held in memory, so
|
|
26
|
+
* `get` is synchronous.
|
|
27
|
+
*
|
|
28
|
+
* Whether the stored token is behind biometrics is kept beside it in the device store (not a
|
|
29
|
+
* secret), since it must be read with the options it was written with.
|
|
30
|
+
*
|
|
31
|
+
* `set` and `clear` tell subscribers: a token cleared anywhere (the core on a 401, a Logout
|
|
32
|
+
* action, the shell's own sign-out) is how the shell hears the session ended. `load` and `unload`
|
|
33
|
+
* (a lock drops the protected token from memory) tell no one.
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} params
|
|
36
|
+
* @param {string} params.appId The app, from shell.json.
|
|
37
|
+
* @param {boolean} params.background The shell does background work (isBackgroundShell).
|
|
38
|
+
* @param {Function} params.getBiometric Whether a new token goes behind biometrics.
|
|
39
|
+
* @param {{ get, set, remove }} params.kv The device store.
|
|
40
|
+
*/
|
|
41
|
+
function createTokenStore({ appId, background, getBiometric, kv }) {
|
|
42
|
+
const key = `lowdefy-session-token.${appId.replace(/[^A-Za-z0-9._-]/g, '_')}`;
|
|
43
|
+
const modeKey = `lowdefy-shell:token-mode:${appId}`;
|
|
44
|
+
const listeners = new Set();
|
|
45
|
+
let token = null;
|
|
46
|
+
|
|
47
|
+
function notify() {
|
|
48
|
+
listeners.forEach((listener) => listener(token));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isProtected() {
|
|
52
|
+
return !background && kv.get(modeKey) === PROTECTED;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
isProtected,
|
|
57
|
+
async load() {
|
|
58
|
+
token = await SecureStore.getItemAsync(
|
|
59
|
+
key,
|
|
60
|
+
getTokenStoreOptions({ background, biometric: isProtected() })
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
get() {
|
|
64
|
+
return token;
|
|
65
|
+
},
|
|
66
|
+
async set(value) {
|
|
67
|
+
const biometric = !background && getBiometric();
|
|
68
|
+
if (biometric !== isProtected()) {
|
|
69
|
+
await SecureStore.deleteItemAsync(
|
|
70
|
+
key,
|
|
71
|
+
getTokenStoreOptions({ background, biometric: !biometric })
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
await SecureStore.setItemAsync(key, value, getTokenStoreOptions({ background, biometric }));
|
|
75
|
+
kv.set(modeKey, biometric ? PROTECTED : 'plain');
|
|
76
|
+
token = value;
|
|
77
|
+
notify();
|
|
78
|
+
},
|
|
79
|
+
unload() {
|
|
80
|
+
token = null;
|
|
81
|
+
},
|
|
82
|
+
async clear() {
|
|
83
|
+
const held = token !== null;
|
|
84
|
+
token = null;
|
|
85
|
+
// Both forms, so a token written before a change of unlock mode never outlives sign-out.
|
|
86
|
+
await SecureStore.deleteItemAsync(key, getTokenStoreOptions({ background, biometric: true }));
|
|
87
|
+
await SecureStore.deleteItemAsync(
|
|
88
|
+
key,
|
|
89
|
+
getTokenStoreOptions({ background, biometric: false })
|
|
90
|
+
);
|
|
91
|
+
kv.remove(modeKey);
|
|
92
|
+
if (held) {
|
|
93
|
+
notify();
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
subscribe(listener) {
|
|
97
|
+
listeners.add(listener);
|
|
98
|
+
return () => listeners.delete(listener);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default createTokenStore;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
import createMemoryStore from '../host/createMemoryStore.js';
|
|
20
|
+
import createTokenStore from './createTokenStore.js';
|
|
21
|
+
|
|
22
|
+
jest.mock('expo-secure-store', () =>
|
|
23
|
+
jest.requireActual('../test/createFakeSecureStore.js').default()
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const SecureStore = jest.requireMock('expo-secure-store');
|
|
27
|
+
|
|
28
|
+
const KEY = 'lowdefy-session-token.field';
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
SecureStore.items.clear();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function create({ background = false, biometric = false, kv = createMemoryStore() } = {}) {
|
|
35
|
+
return {
|
|
36
|
+
kv,
|
|
37
|
+
store: createTokenStore({ appId: 'field', background, getBiometric: () => biometric, kv }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('a shell without background work keeps the token readable only while the phone is unlocked', async () => {
|
|
42
|
+
const { store } = create();
|
|
43
|
+
await store.set('token-1');
|
|
44
|
+
expect(SecureStore.items.get(`${KEY}|no-auth`)).toEqual({
|
|
45
|
+
value: 'token-1',
|
|
46
|
+
options: { keychainAccessible: 'WHEN_UNLOCKED_THIS_DEVICE_ONLY' },
|
|
47
|
+
});
|
|
48
|
+
expect(store.isProtected()).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('with biometric unlock the token is kept behind Face ID or a fingerprint', async () => {
|
|
52
|
+
const { store } = create({ biometric: true });
|
|
53
|
+
await store.set('token-1');
|
|
54
|
+
expect(SecureStore.items.get(`${KEY}|auth`).options).toEqual({
|
|
55
|
+
keychainAccessible: 'WHEN_UNLOCKED_THIS_DEVICE_ONLY',
|
|
56
|
+
requireAuthentication: true,
|
|
57
|
+
authenticationPrompt: 'Unlock to continue',
|
|
58
|
+
});
|
|
59
|
+
expect(store.isProtected()).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('a shell with background work keeps the token readable after first unlock, biometrics or not', async () => {
|
|
63
|
+
const { store } = create({ background: true, biometric: true });
|
|
64
|
+
await store.set('token-1');
|
|
65
|
+
expect(SecureStore.items.get(`${KEY}|no-auth`).options).toEqual({
|
|
66
|
+
keychainAccessible: 'AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY',
|
|
67
|
+
});
|
|
68
|
+
expect(store.isProtected()).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('the token is read once, with the options it was written with, and held in memory', async () => {
|
|
72
|
+
const kv = createMemoryStore();
|
|
73
|
+
await create({ biometric: true, kv }).store.set('token-1');
|
|
74
|
+
const { store } = create({ biometric: true, kv });
|
|
75
|
+
expect(store.get()).toBeNull();
|
|
76
|
+
await store.load();
|
|
77
|
+
expect(SecureStore.getItemAsync).toHaveBeenLastCalledWith(
|
|
78
|
+
KEY,
|
|
79
|
+
expect.objectContaining({ requireAuthentication: true })
|
|
80
|
+
);
|
|
81
|
+
SecureStore.getItemAsync.mockClear();
|
|
82
|
+
expect(store.get()).toBe('token-1');
|
|
83
|
+
expect(store.get()).toBe('token-1');
|
|
84
|
+
expect(SecureStore.getItemAsync).not.toHaveBeenCalled();
|
|
85
|
+
store.unload();
|
|
86
|
+
expect(store.get()).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('a change of unlock mode moves the token, leaving no copy behind', async () => {
|
|
90
|
+
let biometric = false;
|
|
91
|
+
const store = createTokenStore({
|
|
92
|
+
appId: 'field',
|
|
93
|
+
background: false,
|
|
94
|
+
getBiometric: () => biometric,
|
|
95
|
+
kv: createMemoryStore(),
|
|
96
|
+
});
|
|
97
|
+
await store.set('token-1');
|
|
98
|
+
biometric = true;
|
|
99
|
+
await store.set('token-2');
|
|
100
|
+
expect([...SecureStore.items.keys()]).toEqual([`${KEY}|auth`]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('set and clear tell subscribers; clear removes every copy of the token', async () => {
|
|
104
|
+
const { kv, store } = create();
|
|
105
|
+
const listener = jest.fn();
|
|
106
|
+
store.subscribe(listener);
|
|
107
|
+
await store.set('token-1');
|
|
108
|
+
expect(listener).toHaveBeenLastCalledWith('token-1');
|
|
109
|
+
await store.clear();
|
|
110
|
+
expect(listener).toHaveBeenLastCalledWith(null);
|
|
111
|
+
expect(SecureStore.items.size).toBe(0);
|
|
112
|
+
expect(kv.keys()).toEqual([]);
|
|
113
|
+
listener.mockClear();
|
|
114
|
+
await store.clear();
|
|
115
|
+
expect(listener).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// The mobile.auth settings the app's server sends in the mobile root (emailCode, unlock,
|
|
18
|
+
// unlockAfter, sharedDevice), or null when the app has no auth: the shell then opens it without a
|
|
19
|
+
// sign-in screen.
|
|
20
|
+
function getAuthSettings(root) {
|
|
21
|
+
return root?.auth ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default getAuthSettings;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import getAuthSettings from './getAuthSettings.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What the app shows for its auth state and session state:
|
|
21
|
+
* - `lock`: the lock screen, before any page;
|
|
22
|
+
* - `loading`: the token is being read;
|
|
23
|
+
* - `signIn`: the app has auth and nobody is signed in;
|
|
24
|
+
* - `app`: the app's pages (or the session's own loading and error screens).
|
|
25
|
+
*/
|
|
26
|
+
function getAuthView({ auth, session }) {
|
|
27
|
+
if (auth.status === 'locked') {
|
|
28
|
+
return 'lock';
|
|
29
|
+
}
|
|
30
|
+
if (auth.status === 'starting') {
|
|
31
|
+
return 'loading';
|
|
32
|
+
}
|
|
33
|
+
if (session.status === 'ready' && getAuthSettings(session.root) !== null && !auth.hasToken) {
|
|
34
|
+
return 'signIn';
|
|
35
|
+
}
|
|
36
|
+
return 'app';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default getAuthView;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import getAuthView from './getAuthView.js';
|
|
18
|
+
import shouldLock from './shouldLock.js';
|
|
19
|
+
|
|
20
|
+
const settings = { emailCode: true, unlock: 'none', unlockAfter: 300, sharedDevice: false };
|
|
21
|
+
const ready = { status: 'ready', root: { auth: settings } };
|
|
22
|
+
|
|
23
|
+
test('getAuthView shows the lock screen before anything else', () => {
|
|
24
|
+
expect(getAuthView({ auth: { status: 'locked', hasToken: true }, session: ready })).toBe('lock');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('getAuthView waits while the token is read', () => {
|
|
28
|
+
expect(getAuthView({ auth: { status: 'starting', hasToken: false }, session: ready })).toBe(
|
|
29
|
+
'loading'
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('getAuthView asks for sign-in when the app has auth and nobody is signed in', () => {
|
|
34
|
+
expect(getAuthView({ auth: { status: 'open', hasToken: false }, session: ready })).toBe('signIn');
|
|
35
|
+
expect(getAuthView({ auth: { status: 'open', hasToken: true }, session: ready })).toBe('app');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('getAuthView opens an app without auth, and leaves loading and errors to the app', () => {
|
|
39
|
+
const open = { status: 'open', hasToken: false };
|
|
40
|
+
expect(getAuthView({ auth: open, session: { status: 'ready', root: { auth: null } } })).toBe(
|
|
41
|
+
'app'
|
|
42
|
+
);
|
|
43
|
+
expect(getAuthView({ auth: open, session: { status: 'loading', root: null } })).toBe('app');
|
|
44
|
+
expect(getAuthView({ auth: open, session: { status: 'error', root: null } })).toBe('app');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('shouldLock locks after unlockAfter seconds in the background, not before', () => {
|
|
48
|
+
expect(shouldLock({ backgroundedAt: null, now: 1e9, unlockAfter: 0 })).toBe(false);
|
|
49
|
+
expect(shouldLock({ backgroundedAt: 1000, now: 300_999, unlockAfter: 300 })).toBe(false);
|
|
50
|
+
expect(shouldLock({ backgroundedAt: 1000, now: 301_000, unlockAfter: 300 })).toBe(true);
|
|
51
|
+
expect(shouldLock({ backgroundedAt: 1000, now: 1000, unlockAfter: 0 })).toBe(true);
|
|
52
|
+
});
|