@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,83 @@
|
|
|
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 ScrollTo from './ScrollTo.js';
|
|
20
|
+
|
|
21
|
+
const methods = { getPageId: () => 'home' };
|
|
22
|
+
|
|
23
|
+
// A ScrollView 600 high, and a block view 100 high at 1000 in its content.
|
|
24
|
+
function createPage({ withView = true } = {}) {
|
|
25
|
+
const innerView = { name: 'content' };
|
|
26
|
+
const scrollView = {
|
|
27
|
+
getInnerViewRef: () => innerView,
|
|
28
|
+
getNativeScrollRef: () => ({ measure: (callback) => callback(0, 0, 400, 600) }),
|
|
29
|
+
scrollTo: jest.fn(),
|
|
30
|
+
};
|
|
31
|
+
const view = {
|
|
32
|
+
measureLayout: jest.fn((relativeTo, onSuccess) => onSuccess(0, 1000, 400, 100)),
|
|
33
|
+
};
|
|
34
|
+
const registered = {
|
|
35
|
+
'scroll:home:': scrollView,
|
|
36
|
+
...(withView ? { 'view:home:jobs': view } : {}),
|
|
37
|
+
};
|
|
38
|
+
const globals = {
|
|
39
|
+
elements: {
|
|
40
|
+
find: ({ blockId, kind, pageId }) => registered[`${kind}:${pageId}:${blockId ?? ''}`] ?? null,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return { globals, innerView, scrollView, view };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test('ScrollTo throws without params', async () => {
|
|
47
|
+
await expect(ScrollTo({ globals: createPage().globals, methods })).rejects.toThrow(
|
|
48
|
+
'Invalid ScrollTo, check action params.'
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('ScrollTo without a blockId scrolls the page to top, as window.scrollTo does', async () => {
|
|
53
|
+
const { globals, scrollView } = createPage();
|
|
54
|
+
await ScrollTo({ globals, methods, params: { top: 200, behavior: 'smooth' } });
|
|
55
|
+
expect(scrollView.scrollTo.mock.calls).toEqual([[{ animated: true, x: 0, y: 200 }]]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('ScrollTo scrolls a block to the top of the page, measured in the scroll content', async () => {
|
|
59
|
+
const { globals, innerView, scrollView, view } = createPage();
|
|
60
|
+
await ScrollTo({ globals, methods, params: { blockId: 'jobs' } });
|
|
61
|
+
expect(view.measureLayout.mock.calls[0][0]).toBe(innerView);
|
|
62
|
+
expect(scrollView.scrollTo.mock.calls).toEqual([[{ animated: false, x: 0, y: 1000 }]]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('ScrollTo places a block at the center or end of the page by options.block', async () => {
|
|
66
|
+
const { globals, scrollView } = createPage();
|
|
67
|
+
await ScrollTo({
|
|
68
|
+
globals,
|
|
69
|
+
methods,
|
|
70
|
+
params: { blockId: 'jobs', options: { block: 'center', behavior: 'smooth' } },
|
|
71
|
+
});
|
|
72
|
+
await ScrollTo({ globals, methods, params: { blockId: 'jobs', options: { block: 'end' } } });
|
|
73
|
+
expect(scrollView.scrollTo.mock.calls).toEqual([
|
|
74
|
+
[{ animated: true, x: 0, y: 750 }],
|
|
75
|
+
[{ animated: false, x: 0, y: 500 }],
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('ScrollTo leaves the page where it is for a block not on it', async () => {
|
|
80
|
+
const { globals, scrollView } = createPage({ withView: false });
|
|
81
|
+
await ScrollTo({ globals, methods, params: { blockId: 'jobs' } });
|
|
82
|
+
expect(scrollView.scrollTo).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
// The native body of actions-core's SetFocus: focuses the control the block registered as its
|
|
20
|
+
// input (useElementRef kind 'input'). As on the web, a block with nothing to focus is left alone.
|
|
21
|
+
function SetFocus({ globals, methods, params }) {
|
|
22
|
+
if (!type.isString(params)) {
|
|
23
|
+
throw new Error('SetFocus parameter must be a string.');
|
|
24
|
+
}
|
|
25
|
+
const input = globals.elements.find({
|
|
26
|
+
blockId: params,
|
|
27
|
+
kind: 'input',
|
|
28
|
+
pageId: methods.getPageId(),
|
|
29
|
+
});
|
|
30
|
+
input?.focus?.();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default SetFocus;
|
|
@@ -0,0 +1,49 @@
|
|
|
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 SetFocus from './SetFocus.js';
|
|
20
|
+
|
|
21
|
+
function createGlobals(elements) {
|
|
22
|
+
return {
|
|
23
|
+
elements: {
|
|
24
|
+
find: ({ blockId, kind, pageId }) => elements[`${kind}:${pageId}:${blockId}`] ?? null,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const methods = { getPageId: () => 'home' };
|
|
30
|
+
|
|
31
|
+
test('SetFocus focuses the input the block registered on the page', () => {
|
|
32
|
+
const input = { focus: jest.fn() };
|
|
33
|
+
SetFocus({ globals: createGlobals({ 'input:home:name': input }), methods, params: 'name' });
|
|
34
|
+
expect(input.focus).toHaveBeenCalledTimes(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('SetFocus leaves a block with nothing to focus alone', () => {
|
|
38
|
+
const other = { focus: jest.fn() };
|
|
39
|
+
expect(() =>
|
|
40
|
+
SetFocus({ globals: createGlobals({ 'input:other:name': other }), methods, params: 'name' })
|
|
41
|
+
).not.toThrow();
|
|
42
|
+
expect(other.focus).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('SetFocus takes a block id string', () => {
|
|
46
|
+
expect(() => SetFocus({ globals: createGlobals({}), methods, params: { blockId: 'a' } })).toThrow(
|
|
47
|
+
'SetFocus parameter must be a string.'
|
|
48
|
+
);
|
|
49
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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 { serializer, type } from '@lowdefy/helpers';
|
|
18
|
+
import validateStorageKey from '@lowdefy/actions-core/localStorage/validateStorageKey.js';
|
|
19
|
+
|
|
20
|
+
// The native body of actions-core's SetLocalStorage: writes the app's key to the device store
|
|
21
|
+
// (expo-sqlite's key-value store), where the web writes localStorage.
|
|
22
|
+
function SetLocalStorage({ globals, methods, params }) {
|
|
23
|
+
if (!type.isObject(params)) {
|
|
24
|
+
throw new Error('SetLocalStorage params should be an object.');
|
|
25
|
+
}
|
|
26
|
+
const { key, value } = params;
|
|
27
|
+
validateStorageKey({ actionType: 'SetLocalStorage', key });
|
|
28
|
+
if (type.isUndefined(value)) {
|
|
29
|
+
throw new Error('SetLocalStorage "value" is required.');
|
|
30
|
+
}
|
|
31
|
+
globals.storage.set(methods.getAppStorageKey(key), serializer.serializeToString(value));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default SetLocalStorage;
|
|
@@ -0,0 +1,68 @@
|
|
|
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 createMemoryStore from '../host/createMemoryStore.js';
|
|
18
|
+
import GetLocalStorage from './GetLocalStorage.js';
|
|
19
|
+
import RemoveLocalStorage from './RemoveLocalStorage.js';
|
|
20
|
+
import SetLocalStorage from './SetLocalStorage.js';
|
|
21
|
+
|
|
22
|
+
const methods = { getAppStorageKey: (key) => `lowdefy:field::${key}` };
|
|
23
|
+
|
|
24
|
+
function createGlobals() {
|
|
25
|
+
return { storage: createMemoryStore() };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('SetLocalStorage keeps a serialized value under the app key, which GetLocalStorage reads', () => {
|
|
29
|
+
const globals = createGlobals();
|
|
30
|
+
const value = { due: new Date(1600000000000), title: 'Pump' };
|
|
31
|
+
SetLocalStorage({ globals, methods, params: { key: 'draft', value } });
|
|
32
|
+
expect(globals.storage.get('lowdefy:field::draft')).toBe(
|
|
33
|
+
'{"due":{"~d":1600000000000},"title":"Pump"}'
|
|
34
|
+
);
|
|
35
|
+
expect(GetLocalStorage({ globals, methods, params: { key: 'draft' } })).toEqual(value);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('GetLocalStorage gives the default for a key not set, and a plain string as it is', () => {
|
|
39
|
+
const globals = createGlobals();
|
|
40
|
+
expect(GetLocalStorage({ globals, methods, params: { key: 'draft', default: 'none' } })).toBe(
|
|
41
|
+
'none'
|
|
42
|
+
);
|
|
43
|
+
globals.storage.set('lowdefy:field::mode', 'dark');
|
|
44
|
+
expect(GetLocalStorage({ globals, methods, params: { key: 'mode' } })).toBe('dark');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('RemoveLocalStorage removes the app key', () => {
|
|
48
|
+
const globals = createGlobals();
|
|
49
|
+
SetLocalStorage({ globals, methods, params: { key: 'draft', value: 1 } });
|
|
50
|
+
RemoveLocalStorage({ globals, methods, params: { key: 'draft' } });
|
|
51
|
+
expect(globals.storage.keys()).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the local storage actions refuse keys Lowdefy keeps for itself', () => {
|
|
55
|
+
const globals = createGlobals();
|
|
56
|
+
expect(() =>
|
|
57
|
+
SetLocalStorage({ globals, methods, params: { key: 'lowdefy_darkMode', value: 'dark' } })
|
|
58
|
+
).toThrow('SetLocalStorage "key" "lowdefy_darkMode" is reserved.');
|
|
59
|
+
expect(() => GetLocalStorage({ globals, methods, params: { key: '' } })).toThrow(
|
|
60
|
+
'GetLocalStorage "key" should be a non-empty string.'
|
|
61
|
+
);
|
|
62
|
+
expect(() => RemoveLocalStorage({ globals, methods, params: 'draft' })).toThrow(
|
|
63
|
+
'RemoveLocalStorage params should be an object.'
|
|
64
|
+
);
|
|
65
|
+
expect(() => SetLocalStorage({ globals, methods, params: { key: 'draft' } })).toThrow(
|
|
66
|
+
'SetLocalStorage "value" is required.'
|
|
67
|
+
);
|
|
68
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
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 view's position and height in the ScrollView's content, and the height of the ScrollView's
|
|
18
|
+
// visible area.
|
|
19
|
+
function measureInScrollView({ scrollView, view }) {
|
|
20
|
+
const measureElement = new Promise((resolve, reject) => {
|
|
21
|
+
view.measureLayout(
|
|
22
|
+
scrollView.getInnerViewRef(),
|
|
23
|
+
(left, top, width, height) => resolve({ height, top }),
|
|
24
|
+
() => reject(new Error('ScrollTo could not measure the block in the page.'))
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
const measureViewport = new Promise((resolve) => {
|
|
28
|
+
scrollView.getNativeScrollRef().measure((x, y, width, height) => resolve({ height }));
|
|
29
|
+
});
|
|
30
|
+
return Promise.all([measureElement, measureViewport]).then(([element, viewport]) => ({
|
|
31
|
+
element,
|
|
32
|
+
viewport,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default measureInScrollView;
|
|
@@ -0,0 +1,41 @@
|
|
|
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 CopyToClipboard from './CopyToClipboard.js';
|
|
18
|
+
import GeolocationCurrentPosition from './GeolocationCurrentPosition.js';
|
|
19
|
+
import GetLocalStorage from './GetLocalStorage.js';
|
|
20
|
+
import RemoveLocalStorage from './RemoveLocalStorage.js';
|
|
21
|
+
import ScrollTo from './ScrollTo.js';
|
|
22
|
+
import SetFocus from './SetFocus.js';
|
|
23
|
+
import SetLocalStorage from './SetLocalStorage.js';
|
|
24
|
+
|
|
25
|
+
// Native bodies of the actions-core actions whose web bodies reach the DOM, under the same names,
|
|
26
|
+
// so pages call them as on the web. They reach the device through the shell's host globals
|
|
27
|
+
// (createShellGlobals). SetDarkMode and SetLocale keep their actions-core bodies: they call the
|
|
28
|
+
// window members the shell sets (client-native's ThemeProvider and the shell's locale).
|
|
29
|
+
// GeolocationCurrentPosition answers with a location Lowdefy Dev injected until the shell carries
|
|
30
|
+
// device location.
|
|
31
|
+
const nativeActions = {
|
|
32
|
+
CopyToClipboard,
|
|
33
|
+
GeolocationCurrentPosition,
|
|
34
|
+
GetLocalStorage,
|
|
35
|
+
RemoveLocalStorage,
|
|
36
|
+
ScrollTo,
|
|
37
|
+
SetFocus,
|
|
38
|
+
SetLocalStorage,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default nativeActions;
|
|
@@ -0,0 +1,44 @@
|
|
|
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 React from 'react';
|
|
18
|
+
import { Pressable, Text } from 'react-native';
|
|
19
|
+
|
|
20
|
+
import styles from './styles.js';
|
|
21
|
+
import useSurfaceTokens from '../components/useSurfaceTokens.js';
|
|
22
|
+
|
|
23
|
+
function AuthButton({ disabled = false, onPress, testID, title }) {
|
|
24
|
+
const tokens = useSurfaceTokens();
|
|
25
|
+
return (
|
|
26
|
+
<Pressable
|
|
27
|
+
accessibilityRole="button"
|
|
28
|
+
accessibilityState={{ disabled }}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
onPress={onPress}
|
|
31
|
+
style={({ pressed }) => [
|
|
32
|
+
styles.button,
|
|
33
|
+
{ backgroundColor: tokens.colorTint },
|
|
34
|
+
pressed ? { opacity: 0.7 } : null,
|
|
35
|
+
disabled ? styles.buttonDisabled : null,
|
|
36
|
+
]}
|
|
37
|
+
testID={testID}
|
|
38
|
+
>
|
|
39
|
+
<Text style={styles.buttonText}>{title}</Text>
|
|
40
|
+
</Pressable>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default AuthButton;
|
|
@@ -0,0 +1,57 @@
|
|
|
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 React, { useEffect, useSyncExternalStore } from 'react';
|
|
18
|
+
import * as SplashScreen from 'expo-splash-screen';
|
|
19
|
+
|
|
20
|
+
import getAuthSettings from './getAuthSettings.js';
|
|
21
|
+
import getAuthView from './getAuthView.js';
|
|
22
|
+
import LoadingScreen from '../components/LoadingScreen.js';
|
|
23
|
+
import LockScreen from './LockScreen.js';
|
|
24
|
+
import SignInScreen from './SignInScreen.js';
|
|
25
|
+
import { useSessionState, useShell } from '../ShellContext.js';
|
|
26
|
+
|
|
27
|
+
// The lock screen or sign-in screen in place of the app, until it is unlocked and someone is
|
|
28
|
+
// signed in (getAuthView).
|
|
29
|
+
function AuthGate({ children }) {
|
|
30
|
+
const { auth } = useShell();
|
|
31
|
+
const authState = useSyncExternalStore(auth.subscribe, auth.getState);
|
|
32
|
+
const sessionState = useSessionState();
|
|
33
|
+
const view = getAuthView({ auth: authState, session: sessionState });
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (view === 'lock' || view === 'signIn') SplashScreen.hideAsync();
|
|
37
|
+
}, [view]);
|
|
38
|
+
|
|
39
|
+
if (view === 'lock') {
|
|
40
|
+
return <LockScreen appName={auth.getAppName()} auth={auth} />;
|
|
41
|
+
}
|
|
42
|
+
if (view === 'loading') {
|
|
43
|
+
return <LoadingScreen />;
|
|
44
|
+
}
|
|
45
|
+
if (view === 'signIn') {
|
|
46
|
+
return (
|
|
47
|
+
<SignInScreen
|
|
48
|
+
appName={auth.getAppName()}
|
|
49
|
+
auth={auth}
|
|
50
|
+
settings={getAuthSettings(sessionState.root)}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return children;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default AuthGate;
|
|
@@ -0,0 +1,43 @@
|
|
|
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 React from 'react';
|
|
18
|
+
import { Pressable, Text } from 'react-native';
|
|
19
|
+
|
|
20
|
+
import styles from './styles.js';
|
|
21
|
+
import useSurfaceTokens from '../components/useSurfaceTokens.js';
|
|
22
|
+
|
|
23
|
+
function AuthLink({ disabled = false, onPress, style, testID, title }) {
|
|
24
|
+
const tokens = useSurfaceTokens();
|
|
25
|
+
return (
|
|
26
|
+
<Pressable
|
|
27
|
+
accessibilityRole="link"
|
|
28
|
+
accessibilityState={{ disabled }}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
onPress={onPress}
|
|
31
|
+
style={style}
|
|
32
|
+
testID={testID}
|
|
33
|
+
>
|
|
34
|
+
<Text
|
|
35
|
+
style={[styles.link, { color: disabled ? tokens.colorTertiaryLabel : tokens.colorTint }]}
|
|
36
|
+
>
|
|
37
|
+
{title}
|
|
38
|
+
</Text>
|
|
39
|
+
</Pressable>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default AuthLink;
|
|
@@ -0,0 +1,67 @@
|
|
|
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 React, { useCallback, useEffect, useState } from 'react';
|
|
18
|
+
import { View, Text } from 'react-native';
|
|
19
|
+
|
|
20
|
+
import AuthButton from './AuthButton.js';
|
|
21
|
+
import AuthLink from './AuthLink.js';
|
|
22
|
+
import styles from './styles.js';
|
|
23
|
+
import useSurfaceTokens from '../components/useSurfaceTokens.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The lock screen of an app with biometric unlock: no page shows until Face ID or a fingerprint
|
|
27
|
+
* unlocks it. It asks at once, and again on Unlock. "Sign in again" signs out, for a person whose
|
|
28
|
+
* biometrics no longer open the app.
|
|
29
|
+
*/
|
|
30
|
+
function LockScreen({ appName, auth }) {
|
|
31
|
+
const tokens = useSurfaceTokens();
|
|
32
|
+
const [busy, setBusy] = useState(false);
|
|
33
|
+
|
|
34
|
+
const unlock = useCallback(async () => {
|
|
35
|
+
setBusy(true);
|
|
36
|
+
try {
|
|
37
|
+
await auth.unlock();
|
|
38
|
+
} finally {
|
|
39
|
+
setBusy(false);
|
|
40
|
+
}
|
|
41
|
+
}, [auth]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
unlock();
|
|
45
|
+
}, [unlock]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<View
|
|
49
|
+
style={[styles.screen, { backgroundColor: tokens.colorGroupedBackground }]}
|
|
50
|
+
testID="shell-lock"
|
|
51
|
+
>
|
|
52
|
+
<Text accessibilityRole="header" style={[styles.appName, { color: tokens.colorLabel }]}>
|
|
53
|
+
{appName}
|
|
54
|
+
</Text>
|
|
55
|
+
<AuthButton disabled={busy} onPress={unlock} testID="lock-unlock" title="Unlock" />
|
|
56
|
+
<AuthLink
|
|
57
|
+
disabled={busy}
|
|
58
|
+
onPress={() => auth.signOut()}
|
|
59
|
+
style={styles.centered}
|
|
60
|
+
testID="lock-sign-in-again"
|
|
61
|
+
title="Sign in again"
|
|
62
|
+
/>
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default LockScreen;
|
|
@@ -0,0 +1,37 @@
|
|
|
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 React from 'react';
|
|
18
|
+
import { jest } from '@jest/globals';
|
|
19
|
+
import { fireEvent, render, screen } from '@testing-library/react-native';
|
|
20
|
+
|
|
21
|
+
import LockScreen from './LockScreen.js';
|
|
22
|
+
|
|
23
|
+
test('the lock screen asks to unlock at once, and again on Unlock', async () => {
|
|
24
|
+
const auth = { unlock: jest.fn(async () => false), signOut: jest.fn() };
|
|
25
|
+
await render(<LockScreen appName="Field" auth={auth} />);
|
|
26
|
+
expect(screen.getByText('Field')).toBeTruthy();
|
|
27
|
+
expect(auth.unlock).toHaveBeenCalledTimes(1);
|
|
28
|
+
await fireEvent.press(screen.getByTestId('lock-unlock'));
|
|
29
|
+
expect(auth.unlock).toHaveBeenCalledTimes(2);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('Sign in again signs out', async () => {
|
|
33
|
+
const auth = { unlock: jest.fn(async () => false), signOut: jest.fn() };
|
|
34
|
+
await render(<LockScreen appName="Field" auth={auth} />);
|
|
35
|
+
await fireEvent.press(screen.getByTestId('lock-sign-in-again'));
|
|
36
|
+
expect(auth.signOut).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|