@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,35 @@
|
|
|
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
|
+
* Where the sign-in sheet returns to the shell with its one-time token: on iOS the shell's scheme,
|
|
19
|
+
* <scheme>://auth, which ASWebAuthenticationSession hands only to the app that opened the sheet;
|
|
20
|
+
* on Android the app's https app link, <app url>/auth/mobile/callback, which Android opens only in
|
|
21
|
+
* the shell the app's assetlinks.json names (app.config.ts declares it, authCallbackIntentFilters).
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} params
|
|
24
|
+
* @param {string} params.apiBase The app server's URL.
|
|
25
|
+
* @param {'ios'|'android'} params.platform
|
|
26
|
+
* @param {{ scheme }} params.shell The shell config.
|
|
27
|
+
*/
|
|
28
|
+
function getSheetReturnUrl({ apiBase, platform, shell }) {
|
|
29
|
+
if (platform === 'android') {
|
|
30
|
+
return `${apiBase.replace(/\/$/, '')}/auth/mobile/callback`;
|
|
31
|
+
}
|
|
32
|
+
return `${shell.scheme}://auth`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default getSheetReturnUrl;
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
/**
|
|
20
|
+
* How the session token is kept in the Keychain or Keystore: on this device only, never in a
|
|
21
|
+
* backup.
|
|
22
|
+
*
|
|
23
|
+
* - A shell with background work (location reports while the phone is locked) keeps it readable
|
|
24
|
+
* after the phone's first unlock, so its background task can call the server. Biometrics then
|
|
25
|
+
* guard the screens only.
|
|
26
|
+
* - Any other shell keeps it readable only while the phone is unlocked, and, with biometric
|
|
27
|
+
* unlock, behind Face ID or a fingerprint: reading it is the unlock prompt. A change to the
|
|
28
|
+
* phone's biometrics makes it unreadable, and the person signs in again.
|
|
29
|
+
*
|
|
30
|
+
* A protected item is read and deleted with the same requireAuthentication it was written with.
|
|
31
|
+
*/
|
|
32
|
+
function getTokenStoreOptions({ background, biometric }) {
|
|
33
|
+
if (background) {
|
|
34
|
+
return { keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY };
|
|
35
|
+
}
|
|
36
|
+
if (biometric) {
|
|
37
|
+
return {
|
|
38
|
+
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
39
|
+
requireAuthentication: true,
|
|
40
|
+
authenticationPrompt: 'Unlock to continue',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default getTokenStoreOptions;
|
|
@@ -0,0 +1,26 @@
|
|
|
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 shell profile that reports location in the background (shell.json `profile`). Its
|
|
18
|
+
// background task calls the server while the phone is locked, so its session token must stay
|
|
19
|
+
// readable then, and biometrics guard only its screens.
|
|
20
|
+
const BACKGROUND_PROFILES = ['field-tracking'];
|
|
21
|
+
|
|
22
|
+
function isBackgroundShell(shell) {
|
|
23
|
+
return BACKGROUND_PROFILES.includes(shell.profile);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default isBackgroundShell;
|
|
@@ -0,0 +1,35 @@
|
|
|
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 link without its query or fragment. React Native's URL has no working pathname, so the
|
|
18
|
+
// link is cut by hand, as parseQuery reads its query.
|
|
19
|
+
function stripQuery(link) {
|
|
20
|
+
const end = link.search(/[?#]/);
|
|
21
|
+
return end === -1 ? link : link.slice(0, end);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether a link the router is opening is the sign-in sheet's return: the Android app link
|
|
26
|
+
* <app url>/auth/mobile/callback (getSheetReturnUrl). signInWithSheet reads it from the sheet's
|
|
27
|
+
* result, so the router must not open it as a page: its query holds the one-time token.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} link The URL, or the path, Expo Router received.
|
|
30
|
+
*/
|
|
31
|
+
function isSheetReturnLink(link) {
|
|
32
|
+
return /^(https:\/\/[^/]+)?(\/.*)?\/auth\/mobile\/callback\/?$/.test(stripQuery(link));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default isSheetReturnLink;
|
|
@@ -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
|
+
// The query of a URL as { name: value }. React Native's URL has no working searchParams, and the
|
|
18
|
+
// sheet's redirect URL is a custom scheme.
|
|
19
|
+
function parseQuery(url) {
|
|
20
|
+
const start = url.indexOf('?');
|
|
21
|
+
if (start === -1) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
const end = url.indexOf('#', start);
|
|
25
|
+
const query = url.slice(start + 1, end === -1 ? undefined : end);
|
|
26
|
+
const params = {};
|
|
27
|
+
query
|
|
28
|
+
.split('&')
|
|
29
|
+
.filter((part) => part.length > 0)
|
|
30
|
+
.forEach((part) => {
|
|
31
|
+
const separator = part.indexOf('=');
|
|
32
|
+
const name = separator === -1 ? part : part.slice(0, separator);
|
|
33
|
+
const value = separator === -1 ? '' : part.slice(separator + 1);
|
|
34
|
+
params[decodeURIComponent(name)] = decodeURIComponent(value.replace(/\+/g, ' '));
|
|
35
|
+
});
|
|
36
|
+
return params;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default parseQuery;
|
|
@@ -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
|
+
import { appStorageKey } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import getLastCacheKeyName from '../cache/getLastCacheKeyName.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Removes everything the shell keeps for one app's signed-in person: the session token, every
|
|
23
|
+
* cached root and page of the app (all its callers), the last cache key, and the person's keys in
|
|
24
|
+
* the device store (SetLocalStorage's user keys, which the core puts under one prefix per app).
|
|
25
|
+
* Device preferences stay. Nothing a person saw outlives their sign-out.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} params
|
|
28
|
+
* @param {string} params.appId The app.
|
|
29
|
+
* @param {Object} params.cacheStore The page cache.
|
|
30
|
+
* @param {{ keys, remove }} params.kv The device store.
|
|
31
|
+
* @param {string|null} params.slug The app's slug (lowdefyApp.slug), which names its storage keys.
|
|
32
|
+
* @param {{ clear }} params.tokenStore The session token store.
|
|
33
|
+
*/
|
|
34
|
+
async function purgeAppData({ appId, cacheStore, kv, slug, tokenStore }) {
|
|
35
|
+
await tokenStore.clear();
|
|
36
|
+
await cacheStore.removeApp({ appId });
|
|
37
|
+
kv.remove(getLastCacheKeyName(appId));
|
|
38
|
+
// The shell renders every page with basePath '' (PageScreen).
|
|
39
|
+
const userPrefix = appStorageKey({ basePath: '', name: 'lowdefy_user:', slug });
|
|
40
|
+
kv.keys()
|
|
41
|
+
.filter((key) => key.startsWith(userPrefix))
|
|
42
|
+
.forEach((key) => kv.remove(key));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default purgeAppData;
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
// Whether the app locks on coming back: it was in the background for unlockAfter seconds or
|
|
18
|
+
// longer. backgroundedAt is null when it never left.
|
|
19
|
+
function shouldLock({ backgroundedAt, now, unlockAfter }) {
|
|
20
|
+
if (backgroundedAt === null) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return now - backgroundedAt >= unlockAfter * 1000;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default shouldLock;
|
|
@@ -0,0 +1,97 @@
|
|
|
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 Crypto from 'expo-crypto';
|
|
18
|
+
import * as WebBrowser from 'expo-web-browser';
|
|
19
|
+
import { Platform } from 'react-native';
|
|
20
|
+
import { createPkceChallenge } from '@lowdefy/client-core';
|
|
21
|
+
|
|
22
|
+
import getSheetReturnUrl from './getSheetReturnUrl.js';
|
|
23
|
+
import parseQuery from './parseQuery.js';
|
|
24
|
+
|
|
25
|
+
const VERIFIER_BYTES = 64;
|
|
26
|
+
|
|
27
|
+
function sha256(data) {
|
|
28
|
+
return Crypto.digest(Crypto.CryptoDigestAlgorithm.SHA256, data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Signs in through the app's own sign-in page in a system sheet: every method the web app offers
|
|
33
|
+
* (passkeys, SSO, magic links, a second factor).
|
|
34
|
+
*
|
|
35
|
+
* - The sheet opens /auth/mobile with a random state, a PKCE S256 challenge and the platform. On
|
|
36
|
+
* iOS it is ephemeral (no cookies shared with Safari).
|
|
37
|
+
* - Once the person is signed in, the page asks them to confirm. On Continue the server returns a
|
|
38
|
+
* one-time token for a new phone session, bound to the challenge: on iOS to
|
|
39
|
+
* <scheme>://auth?token=&state=, which ASWebAuthenticationSession hands only to this app; on
|
|
40
|
+
* Android to the verified app link <app url>/auth/mobile/callback?token=&state=, which Android
|
|
41
|
+
* opens only in this shell (getSheetReturnUrl).
|
|
42
|
+
* - The shell checks state is its own, then exchanges the token with the code verifier for the
|
|
43
|
+
* session token. A return with another state is refused, so a link this sign-in did not start is
|
|
44
|
+
* never exchanged. The verifier lives in this call only: it is in no URL and never stored.
|
|
45
|
+
*
|
|
46
|
+
* Returns { status }: `signedIn`, `cancelled` (the person closed the sheet or pressed Cancel) or
|
|
47
|
+
* `failed`.
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} params
|
|
50
|
+
* @param {string} params.apiBase The app server's URL.
|
|
51
|
+
* @param {Object} params.authApi createAuthApi.
|
|
52
|
+
* @param {{ id, scheme }} params.shell The shell config.
|
|
53
|
+
*/
|
|
54
|
+
async function signInWithSheet({ apiBase, authApi, shell }) {
|
|
55
|
+
const state = Crypto.randomUUID();
|
|
56
|
+
const { codeVerifier, codeChallenge, codeChallengeMethod } = await createPkceChallenge({
|
|
57
|
+
verifierBytes: Crypto.getRandomBytes(VERIFIER_BYTES),
|
|
58
|
+
sha256,
|
|
59
|
+
});
|
|
60
|
+
const platform = Platform.OS === 'android' ? 'android' : 'ios';
|
|
61
|
+
const query = [
|
|
62
|
+
['shell', shell.id],
|
|
63
|
+
['state', state],
|
|
64
|
+
['code_challenge', codeChallenge],
|
|
65
|
+
['code_challenge_method', codeChallengeMethod],
|
|
66
|
+
['platform', platform],
|
|
67
|
+
]
|
|
68
|
+
.map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
|
|
69
|
+
.join('&');
|
|
70
|
+
const returnUrl = getSheetReturnUrl({ apiBase, platform, shell });
|
|
71
|
+
const result = await WebBrowser.openAuthSessionAsync(
|
|
72
|
+
`${apiBase}/auth/mobile?${query}`,
|
|
73
|
+
returnUrl,
|
|
74
|
+
{
|
|
75
|
+
preferEphemeralSession: true,
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
if (result.type !== 'success') {
|
|
79
|
+
return { status: 'cancelled' };
|
|
80
|
+
}
|
|
81
|
+
if (!result.url.startsWith(returnUrl)) {
|
|
82
|
+
return { status: 'failed' };
|
|
83
|
+
}
|
|
84
|
+
const params = parseQuery(result.url);
|
|
85
|
+
if (params.state !== state) {
|
|
86
|
+
return { status: 'failed' };
|
|
87
|
+
}
|
|
88
|
+
if (params.error === 'cancelled') {
|
|
89
|
+
return { status: 'cancelled' };
|
|
90
|
+
}
|
|
91
|
+
if (!params.token) {
|
|
92
|
+
return { status: 'failed' };
|
|
93
|
+
}
|
|
94
|
+
return authApi.exchangeToken({ token: params.token, codeVerifier });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default signInWithSheet;
|
|
@@ -0,0 +1,205 @@
|
|
|
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 { createHash } from 'node:crypto';
|
|
18
|
+
import { jest } from '@jest/globals';
|
|
19
|
+
import { Platform } from 'react-native';
|
|
20
|
+
|
|
21
|
+
import getSheetReturnUrl from './getSheetReturnUrl.js';
|
|
22
|
+
import isSheetReturnLink from './isSheetReturnLink.js';
|
|
23
|
+
import parseQuery from './parseQuery.js';
|
|
24
|
+
import signInWithSheet from './signInWithSheet.js';
|
|
25
|
+
|
|
26
|
+
jest.mock('expo-crypto', () => {
|
|
27
|
+
const nodeCrypto = jest.requireActual('node:crypto');
|
|
28
|
+
return {
|
|
29
|
+
CryptoDigestAlgorithm: { SHA256: 'SHA-256' },
|
|
30
|
+
digest: jest.fn(async (algorithm, data) =>
|
|
31
|
+
nodeCrypto.createHash('sha256').update(data).digest()
|
|
32
|
+
),
|
|
33
|
+
getRandomBytes: jest.fn((count) => new Uint8Array(nodeCrypto.randomBytes(count))),
|
|
34
|
+
randomUUID: jest.fn(() => 'state-1'),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
jest.mock('expo-web-browser', () => ({ openAuthSessionAsync: jest.fn() }));
|
|
39
|
+
|
|
40
|
+
const WebBrowser = jest.requireMock('expo-web-browser');
|
|
41
|
+
|
|
42
|
+
const apiBase = 'https://field.example.com';
|
|
43
|
+
const shell = { id: 'field', scheme: 'com.example.field' };
|
|
44
|
+
|
|
45
|
+
function base64Url(buffer) {
|
|
46
|
+
return Buffer.from(buffer).toString('base64url');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createAuthApi() {
|
|
50
|
+
return { exchangeToken: jest.fn(async () => ({ status: 'signedIn' })) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
jest.restoreAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function onAndroid() {
|
|
58
|
+
jest.replaceProperty(Platform, 'OS', 'android');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test('the sheet opens /auth/mobile ephemeral, with the shell, a state, an S256 challenge and the platform', async () => {
|
|
62
|
+
WebBrowser.openAuthSessionAsync.mockResolvedValue({
|
|
63
|
+
type: 'success',
|
|
64
|
+
url: 'com.example.field://auth?token=ott-1&state=state-1',
|
|
65
|
+
});
|
|
66
|
+
const authApi = createAuthApi();
|
|
67
|
+
expect(await signInWithSheet({ apiBase, authApi, shell })).toEqual({ status: 'signedIn' });
|
|
68
|
+
const [url, redirectUrl, options] = WebBrowser.openAuthSessionAsync.mock.calls[0];
|
|
69
|
+
expect(url.startsWith(`${apiBase}/auth/mobile?`)).toBe(true);
|
|
70
|
+
const query = parseQuery(url);
|
|
71
|
+
expect(query.shell).toBe('field');
|
|
72
|
+
expect(query.state).toBe('state-1');
|
|
73
|
+
expect(query.code_challenge_method).toBe('S256');
|
|
74
|
+
expect(query.code_challenge).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
|
75
|
+
expect(query.platform).toBe('ios');
|
|
76
|
+
expect(redirectUrl).toBe('com.example.field://auth');
|
|
77
|
+
expect(options).toEqual({ preferEphemeralSession: true });
|
|
78
|
+
|
|
79
|
+
// The verifier goes only to the exchange, and it is the one the challenge was made from.
|
|
80
|
+
const { token, codeVerifier } = authApi.exchangeToken.mock.calls[0][0];
|
|
81
|
+
expect(token).toBe('ott-1');
|
|
82
|
+
expect(codeVerifier).toHaveLength(86);
|
|
83
|
+
expect(base64Url(createHash('sha256').update(codeVerifier).digest())).toBe(query.code_challenge);
|
|
84
|
+
expect(url).not.toContain(codeVerifier);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('each sign-in makes a new verifier, which stays in the call that made it', async () => {
|
|
88
|
+
WebBrowser.openAuthSessionAsync.mockResolvedValue({
|
|
89
|
+
type: 'success',
|
|
90
|
+
url: 'com.example.field://auth?token=ott-1&state=state-1',
|
|
91
|
+
});
|
|
92
|
+
const authApi = createAuthApi();
|
|
93
|
+
await signInWithSheet({ apiBase, authApi, shell });
|
|
94
|
+
await signInWithSheet({ apiBase, authApi, shell });
|
|
95
|
+
const [first, second] = authApi.exchangeToken.mock.calls.map(([params]) => params.codeVerifier);
|
|
96
|
+
expect(first).not.toBe(second);
|
|
97
|
+
// No call out of the sign-in but the exchange carries the verifier: not the sheet, not storage.
|
|
98
|
+
const Crypto = jest.requireMock('expo-crypto');
|
|
99
|
+
const otherCalls = JSON.stringify([
|
|
100
|
+
WebBrowser.openAuthSessionAsync.mock.calls,
|
|
101
|
+
Crypto.randomUUID.mock.calls,
|
|
102
|
+
Crypto.getRandomBytes.mock.calls,
|
|
103
|
+
]);
|
|
104
|
+
expect(otherCalls).not.toContain(first);
|
|
105
|
+
expect(otherCalls).not.toContain(second);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('a redirect with another state is refused and the token is not exchanged', async () => {
|
|
109
|
+
WebBrowser.openAuthSessionAsync.mockResolvedValue({
|
|
110
|
+
type: 'success',
|
|
111
|
+
url: 'com.example.field://auth?token=ott-1&state=someone-else',
|
|
112
|
+
});
|
|
113
|
+
const authApi = createAuthApi();
|
|
114
|
+
expect(await signInWithSheet({ apiBase, authApi, shell })).toEqual({ status: 'failed' });
|
|
115
|
+
expect(authApi.exchangeToken).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('a redirect without a token fails', async () => {
|
|
119
|
+
WebBrowser.openAuthSessionAsync.mockResolvedValue({
|
|
120
|
+
type: 'success',
|
|
121
|
+
url: 'com.example.field://auth?state=state-1',
|
|
122
|
+
});
|
|
123
|
+
const authApi = createAuthApi();
|
|
124
|
+
expect(await signInWithSheet({ apiBase, authApi, shell })).toEqual({ status: 'failed' });
|
|
125
|
+
expect(authApi.exchangeToken).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('on Android the sheet returns by the https app link and exchanges its token', async () => {
|
|
129
|
+
onAndroid();
|
|
130
|
+
WebBrowser.openAuthSessionAsync.mockResolvedValue({
|
|
131
|
+
type: 'success',
|
|
132
|
+
url: `${apiBase}/auth/mobile/callback?token=ott-1&state=state-1`,
|
|
133
|
+
});
|
|
134
|
+
const authApi = createAuthApi();
|
|
135
|
+
expect(await signInWithSheet({ apiBase, authApi, shell })).toEqual({ status: 'signedIn' });
|
|
136
|
+
const [url, redirectUrl] = WebBrowser.openAuthSessionAsync.mock.calls[0];
|
|
137
|
+
expect(parseQuery(url).platform).toBe('android');
|
|
138
|
+
expect(redirectUrl).toBe(`${apiBase}/auth/mobile/callback`);
|
|
139
|
+
expect(authApi.exchangeToken.mock.calls[0][0].token).toBe('ott-1');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('on Android a callback with another state is refused and the token is not exchanged', async () => {
|
|
143
|
+
onAndroid();
|
|
144
|
+
WebBrowser.openAuthSessionAsync.mockResolvedValue({
|
|
145
|
+
type: 'success',
|
|
146
|
+
url: `${apiBase}/auth/mobile/callback?token=ott-1&state=someone-else`,
|
|
147
|
+
});
|
|
148
|
+
const authApi = createAuthApi();
|
|
149
|
+
expect(await signInWithSheet({ apiBase, authApi, shell })).toEqual({ status: 'failed' });
|
|
150
|
+
expect(authApi.exchangeToken).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('on Android a return to the custom scheme is not the app link and is refused', async () => {
|
|
154
|
+
onAndroid();
|
|
155
|
+
WebBrowser.openAuthSessionAsync.mockResolvedValue({
|
|
156
|
+
type: 'success',
|
|
157
|
+
url: 'com.example.field://auth?token=ott-1&state=state-1',
|
|
158
|
+
});
|
|
159
|
+
const authApi = createAuthApi();
|
|
160
|
+
expect(await signInWithSheet({ apiBase, authApi, shell })).toEqual({ status: 'failed' });
|
|
161
|
+
expect(authApi.exchangeToken).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('Cancel on the confirmation page cancels the sign-in', async () => {
|
|
165
|
+
WebBrowser.openAuthSessionAsync.mockResolvedValue({
|
|
166
|
+
type: 'success',
|
|
167
|
+
url: 'com.example.field://auth?error=cancelled&state=state-1',
|
|
168
|
+
});
|
|
169
|
+
const authApi = createAuthApi();
|
|
170
|
+
expect(await signInWithSheet({ apiBase, authApi, shell })).toEqual({ status: 'cancelled' });
|
|
171
|
+
expect(authApi.exchangeToken).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('getSheetReturnUrl is the scheme on iOS and the app link under the app url on Android', () => {
|
|
175
|
+
expect(getSheetReturnUrl({ apiBase, platform: 'ios', shell })).toBe('com.example.field://auth');
|
|
176
|
+
expect(getSheetReturnUrl({ apiBase: `${apiBase}/app/`, platform: 'android', shell })).toBe(
|
|
177
|
+
`${apiBase}/app/auth/mobile/callback`
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('isSheetReturnLink tells the app link return from page links', () => {
|
|
182
|
+
expect(isSheetReturnLink(`${apiBase}/auth/mobile/callback?token=ott-1&state=s`)).toBe(true);
|
|
183
|
+
expect(isSheetReturnLink(`${apiBase}/app/auth/mobile/callback`)).toBe(true);
|
|
184
|
+
expect(isSheetReturnLink('/auth/mobile/callback?token=ott-1')).toBe(true);
|
|
185
|
+
expect(isSheetReturnLink(`${apiBase}/auth/mobile`)).toBe(false);
|
|
186
|
+
expect(isSheetReturnLink(`${apiBase}/jobs?next=/auth/mobile/callback`)).toBe(false);
|
|
187
|
+
expect(isSheetReturnLink('/jobs/42')).toBe(false);
|
|
188
|
+
expect(isSheetReturnLink('com.example.field://jobs')).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('closing the sheet cancels the sign-in', async () => {
|
|
192
|
+
WebBrowser.openAuthSessionAsync.mockResolvedValue({ type: 'cancel' });
|
|
193
|
+
const authApi = createAuthApi();
|
|
194
|
+
expect(await signInWithSheet({ apiBase, authApi, shell })).toEqual({ status: 'cancelled' });
|
|
195
|
+
expect(authApi.exchangeToken).not.toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('parseQuery reads a custom-scheme URL query, decoding each value', () => {
|
|
199
|
+
expect(parseQuery('com.example.field://auth?token=a%2Bb&state=x+y&empty#frag')).toEqual({
|
|
200
|
+
token: 'a+b',
|
|
201
|
+
state: 'x y',
|
|
202
|
+
empty: '',
|
|
203
|
+
});
|
|
204
|
+
expect(parseQuery('com.example.field://auth')).toEqual({});
|
|
205
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { StyleSheet } from 'react-native';
|
|
18
|
+
|
|
19
|
+
const styles = StyleSheet.create({
|
|
20
|
+
screen: {
|
|
21
|
+
flexGrow: 1,
|
|
22
|
+
justifyContent: 'center',
|
|
23
|
+
padding: 24,
|
|
24
|
+
gap: 16,
|
|
25
|
+
},
|
|
26
|
+
appName: {
|
|
27
|
+
fontSize: 28,
|
|
28
|
+
fontWeight: '700',
|
|
29
|
+
textAlign: 'center',
|
|
30
|
+
marginBottom: 8,
|
|
31
|
+
},
|
|
32
|
+
label: {
|
|
33
|
+
fontSize: 16,
|
|
34
|
+
},
|
|
35
|
+
input: {
|
|
36
|
+
borderRadius: 10,
|
|
37
|
+
paddingHorizontal: 16,
|
|
38
|
+
paddingVertical: 12,
|
|
39
|
+
fontSize: 17,
|
|
40
|
+
},
|
|
41
|
+
codeInput: {
|
|
42
|
+
letterSpacing: 8,
|
|
43
|
+
textAlign: 'center',
|
|
44
|
+
fontSize: 24,
|
|
45
|
+
},
|
|
46
|
+
message: {
|
|
47
|
+
fontSize: 15,
|
|
48
|
+
},
|
|
49
|
+
error: {
|
|
50
|
+
fontSize: 15,
|
|
51
|
+
},
|
|
52
|
+
button: {
|
|
53
|
+
alignItems: 'center',
|
|
54
|
+
paddingVertical: 14,
|
|
55
|
+
borderRadius: 12,
|
|
56
|
+
},
|
|
57
|
+
buttonDisabled: {
|
|
58
|
+
opacity: 0.5,
|
|
59
|
+
},
|
|
60
|
+
buttonText: {
|
|
61
|
+
color: '#FFFFFF',
|
|
62
|
+
fontSize: 17,
|
|
63
|
+
fontWeight: '600',
|
|
64
|
+
},
|
|
65
|
+
links: {
|
|
66
|
+
flexDirection: 'row',
|
|
67
|
+
justifyContent: 'space-between',
|
|
68
|
+
},
|
|
69
|
+
link: {
|
|
70
|
+
fontSize: 17,
|
|
71
|
+
paddingVertical: 8,
|
|
72
|
+
},
|
|
73
|
+
centered: {
|
|
74
|
+
alignSelf: 'center',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export default styles;
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
// Unregisters this phone's push token for the app at sign-out, so no notification for the person
|
|
18
|
+
// reaches it afterwards. Push arrives with its own design; until then there is nothing to remove.
|
|
19
|
+
async function unregisterPushToken() {}
|
|
20
|
+
|
|
21
|
+
export default unregisterPushToken;
|