@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,256 @@
|
|
|
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, useRef, useState } from 'react';
|
|
18
|
+
import { KeyboardAvoidingView, Platform, ScrollView, Text, TextInput, View } 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
|
+
const CODE_LENGTH = 6;
|
|
26
|
+
const RESEND_SECONDS = 30;
|
|
27
|
+
|
|
28
|
+
const MESSAGES = {
|
|
29
|
+
sent: 'If this email can sign in, a code is on its way.',
|
|
30
|
+
invalid: 'That code is not right, or it has expired.',
|
|
31
|
+
rateLimited: 'Too many tries. Wait a minute, then try again.',
|
|
32
|
+
sheetFailed: 'Sign-in did not finish. Try again.',
|
|
33
|
+
unreachable: "Could not reach the app's server. Check your connection and try again.",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sign-in for an app with auth, before any page config can be fetched. By default the person
|
|
38
|
+
* types their email, then the 6-digit code from the email (the phone offers it from Mail or
|
|
39
|
+
* Messages). "Other ways to sign in", an app without email codes (mobile.auth.emailCode: false)
|
|
40
|
+
* and a person who must pass a second factor go to the app's own sign-in page in a system sheet.
|
|
41
|
+
*
|
|
42
|
+
* The screen never says whether an address can sign in: after Continue it always says a code is
|
|
43
|
+
* on its way, if the address can sign in.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} props
|
|
46
|
+
* @param {string} props.appName The app's name.
|
|
47
|
+
* @param {Object} props.auth The app's auth (createAppAuth).
|
|
48
|
+
* @param {{ emailCode: boolean }} props.settings The app's mobile.auth.
|
|
49
|
+
*/
|
|
50
|
+
function SignInScreen({ appName, auth, settings }) {
|
|
51
|
+
const tokens = useSurfaceTokens();
|
|
52
|
+
const labelStyle = [styles.label, { color: tokens.colorLabel }];
|
|
53
|
+
const inputStyle = [
|
|
54
|
+
styles.input,
|
|
55
|
+
{ backgroundColor: tokens.colorCell, color: tokens.colorLabel },
|
|
56
|
+
];
|
|
57
|
+
const [step, setStep] = useState('email');
|
|
58
|
+
const [email, setEmail] = useState('');
|
|
59
|
+
const [code, setCode] = useState('');
|
|
60
|
+
const [busy, setBusy] = useState(false);
|
|
61
|
+
const [message, setMessage] = useState(null);
|
|
62
|
+
const [error, setError] = useState(null);
|
|
63
|
+
const [resendIn, setResendIn] = useState(0);
|
|
64
|
+
const sheetOpened = useRef(false);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (resendIn <= 0) return undefined;
|
|
68
|
+
const timer = setTimeout(() => setResendIn((seconds) => seconds - 1), 1000);
|
|
69
|
+
return () => clearTimeout(timer);
|
|
70
|
+
}, [resendIn]);
|
|
71
|
+
|
|
72
|
+
const run = useCallback(async (task) => {
|
|
73
|
+
setBusy(true);
|
|
74
|
+
setError(null);
|
|
75
|
+
try {
|
|
76
|
+
await task();
|
|
77
|
+
} catch (taskError) {
|
|
78
|
+
setError(MESSAGES.unreachable);
|
|
79
|
+
} finally {
|
|
80
|
+
setBusy(false);
|
|
81
|
+
}
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const openSheet = useCallback(async () => {
|
|
85
|
+
const result = await auth.signInWithSheet();
|
|
86
|
+
if (result.status === 'failed') {
|
|
87
|
+
setError(MESSAGES.sheetFailed);
|
|
88
|
+
}
|
|
89
|
+
}, [auth]);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (settings.emailCode || sheetOpened.current) return;
|
|
93
|
+
sheetOpened.current = true;
|
|
94
|
+
run(openSheet);
|
|
95
|
+
}, [openSheet, run, settings.emailCode]);
|
|
96
|
+
|
|
97
|
+
function sendCode() {
|
|
98
|
+
run(async () => {
|
|
99
|
+
const result = await auth.sendCode({ email: email.trim() });
|
|
100
|
+
if (result.status === 'unavailable') {
|
|
101
|
+
await openSheet();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (result.status === 'rateLimited') {
|
|
105
|
+
setError(MESSAGES.rateLimited);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
setStep('code');
|
|
109
|
+
setCode('');
|
|
110
|
+
setMessage(MESSAGES.sent);
|
|
111
|
+
setResendIn(RESEND_SECONDS);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function submitCode(otp) {
|
|
116
|
+
run(async () => {
|
|
117
|
+
const result = await auth.signInWithCode({ email: email.trim(), otp });
|
|
118
|
+
if (result.status === 'twoFactor') {
|
|
119
|
+
await openSheet();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (result.status === 'invalid' || result.status === 'rateLimited') {
|
|
123
|
+
setCode('');
|
|
124
|
+
setError(MESSAGES[result.status]);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function changeCode(text) {
|
|
130
|
+
const digits = text.replace(/\D/g, '').slice(0, CODE_LENGTH);
|
|
131
|
+
setCode(digits);
|
|
132
|
+
if (digits.length === CODE_LENGTH && !busy) {
|
|
133
|
+
submitCode(digits);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function changeEmail() {
|
|
138
|
+
setStep('email');
|
|
139
|
+
setCode('');
|
|
140
|
+
setMessage(null);
|
|
141
|
+
setError(null);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let form;
|
|
145
|
+
if (!settings.emailCode) {
|
|
146
|
+
form = (
|
|
147
|
+
<AuthButton
|
|
148
|
+
disabled={busy}
|
|
149
|
+
onPress={() => run(openSheet)}
|
|
150
|
+
testID="sign-in-sheet"
|
|
151
|
+
title="Sign in"
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
} else if (step === 'email') {
|
|
155
|
+
form = (
|
|
156
|
+
<>
|
|
157
|
+
<Text style={labelStyle}>Email</Text>
|
|
158
|
+
<TextInput
|
|
159
|
+
autoCapitalize="none"
|
|
160
|
+
autoComplete="email"
|
|
161
|
+
autoCorrect={false}
|
|
162
|
+
editable={!busy}
|
|
163
|
+
inputMode="email"
|
|
164
|
+
keyboardType="email-address"
|
|
165
|
+
onChangeText={setEmail}
|
|
166
|
+
onSubmitEditing={sendCode}
|
|
167
|
+
placeholderTextColor={tokens.colorTertiaryLabel}
|
|
168
|
+
returnKeyType="next"
|
|
169
|
+
style={inputStyle}
|
|
170
|
+
testID="sign-in-email"
|
|
171
|
+
textContentType="emailAddress"
|
|
172
|
+
value={email}
|
|
173
|
+
/>
|
|
174
|
+
<AuthButton
|
|
175
|
+
disabled={busy || email.trim().length === 0}
|
|
176
|
+
onPress={sendCode}
|
|
177
|
+
testID="sign-in-continue"
|
|
178
|
+
title="Continue"
|
|
179
|
+
/>
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
form = (
|
|
184
|
+
<>
|
|
185
|
+
<Text
|
|
186
|
+
style={[styles.message, { color: tokens.colorSecondaryLabel }]}
|
|
187
|
+
testID="sign-in-message"
|
|
188
|
+
>
|
|
189
|
+
{message}
|
|
190
|
+
</Text>
|
|
191
|
+
<Text style={labelStyle}>Code sent to {email.trim()}</Text>
|
|
192
|
+
<TextInput
|
|
193
|
+
autoComplete="one-time-code"
|
|
194
|
+
autoFocus
|
|
195
|
+
editable={!busy}
|
|
196
|
+
inputMode="numeric"
|
|
197
|
+
keyboardType="number-pad"
|
|
198
|
+
maxLength={CODE_LENGTH}
|
|
199
|
+
onChangeText={changeCode}
|
|
200
|
+
style={[inputStyle, styles.codeInput]}
|
|
201
|
+
testID="sign-in-code"
|
|
202
|
+
textContentType="oneTimeCode"
|
|
203
|
+
value={code}
|
|
204
|
+
/>
|
|
205
|
+
<View style={styles.links}>
|
|
206
|
+
<AuthLink
|
|
207
|
+
disabled={busy || resendIn > 0}
|
|
208
|
+
onPress={sendCode}
|
|
209
|
+
testID="sign-in-resend"
|
|
210
|
+
title={resendIn > 0 ? `Resend in ${resendIn}s` : 'Resend code'}
|
|
211
|
+
/>
|
|
212
|
+
<AuthLink
|
|
213
|
+
disabled={busy}
|
|
214
|
+
onPress={changeEmail}
|
|
215
|
+
testID="sign-in-change-email"
|
|
216
|
+
title="Change email"
|
|
217
|
+
/>
|
|
218
|
+
</View>
|
|
219
|
+
</>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<KeyboardAvoidingView
|
|
225
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
226
|
+
style={{ backgroundColor: tokens.colorGroupedBackground, flex: 1 }}
|
|
227
|
+
>
|
|
228
|
+
<ScrollView contentContainerStyle={styles.screen} keyboardShouldPersistTaps="handled">
|
|
229
|
+
<Text accessibilityRole="header" style={[styles.appName, { color: tokens.colorLabel }]}>
|
|
230
|
+
{appName}
|
|
231
|
+
</Text>
|
|
232
|
+
{form}
|
|
233
|
+
{error ? (
|
|
234
|
+
<Text
|
|
235
|
+
accessibilityRole="alert"
|
|
236
|
+
style={[styles.error, { color: tokens.colorError }]}
|
|
237
|
+
testID="sign-in-error"
|
|
238
|
+
>
|
|
239
|
+
{error}
|
|
240
|
+
</Text>
|
|
241
|
+
) : null}
|
|
242
|
+
{settings.emailCode ? (
|
|
243
|
+
<AuthLink
|
|
244
|
+
disabled={busy}
|
|
245
|
+
onPress={() => run(openSheet)}
|
|
246
|
+
style={styles.centered}
|
|
247
|
+
testID="sign-in-other"
|
|
248
|
+
title="Other ways to sign in"
|
|
249
|
+
/>
|
|
250
|
+
) : null}
|
|
251
|
+
</ScrollView>
|
|
252
|
+
</KeyboardAvoidingView>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default SignInScreen;
|
|
@@ -0,0 +1,182 @@
|
|
|
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 { act, fireEvent, render, screen } from '@testing-library/react-native';
|
|
20
|
+
|
|
21
|
+
import SignInScreen from './SignInScreen.js';
|
|
22
|
+
|
|
23
|
+
const NEUTRAL = 'If this email can sign in, a code is on its way.';
|
|
24
|
+
|
|
25
|
+
function createAuth(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
sendCode: jest.fn(async () => ({ status: 'sent' })),
|
|
28
|
+
signInWithCode: jest.fn(async () => ({ status: 'signedIn' })),
|
|
29
|
+
signInWithSheet: jest.fn(async () => ({ status: 'cancelled' })),
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function renderScreen({ auth = createAuth(), emailCode = true } = {}) {
|
|
35
|
+
await render(
|
|
36
|
+
<SignInScreen appName="Field" auth={auth} settings={{ emailCode, unlock: 'none' }} />
|
|
37
|
+
);
|
|
38
|
+
return auth;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function enterEmail(email = ' ana@example.com ') {
|
|
42
|
+
await fireEvent.changeText(screen.getByTestId('sign-in-email'), email);
|
|
43
|
+
await fireEvent.press(screen.getByTestId('sign-in-continue'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isDisabled(testID) {
|
|
47
|
+
return screen.getByTestId(testID).props.accessibilityState?.disabled === true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
jest.useRealTimers();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the screen shows the app name and an email field, with the sheet as the other way in', async () => {
|
|
55
|
+
await renderScreen();
|
|
56
|
+
expect(screen.getByText('Field')).toBeTruthy();
|
|
57
|
+
const email = screen.getByTestId('sign-in-email');
|
|
58
|
+
expect(email.props.textContentType).toBe('emailAddress');
|
|
59
|
+
expect(email.props.autoComplete).toBe('email');
|
|
60
|
+
expect(isDisabled('sign-in-continue')).toBe(true);
|
|
61
|
+
expect(screen.getByText('Other ways to sign in')).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('Continue sends a code to the email and asks for it, saying only that it may be on its way', async () => {
|
|
65
|
+
const auth = await renderScreen();
|
|
66
|
+
await enterEmail();
|
|
67
|
+
expect(auth.sendCode).toHaveBeenCalledWith({ email: 'ana@example.com' });
|
|
68
|
+
expect(screen.getByTestId('sign-in-message')).toHaveTextContent(NEUTRAL);
|
|
69
|
+
const code = screen.getByTestId('sign-in-code');
|
|
70
|
+
expect(code.props.textContentType).toBe('oneTimeCode');
|
|
71
|
+
expect(code.props.autoComplete).toBe('one-time-code');
|
|
72
|
+
expect(code.props.maxLength).toBe(6);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('an address that can not sign in gets the same message, and no hint it is unknown', async () => {
|
|
76
|
+
// The server answers every address alike; the screen has nothing else to say.
|
|
77
|
+
const auth = await renderScreen();
|
|
78
|
+
await enterEmail('stranger@example.com');
|
|
79
|
+
expect(auth.sendCode).toHaveBeenCalledWith({ email: 'stranger@example.com' });
|
|
80
|
+
expect(screen.getByTestId('sign-in-message')).toHaveTextContent(NEUTRAL);
|
|
81
|
+
expect(screen.queryByTestId('sign-in-error')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('the sixth digit of the code signs in', async () => {
|
|
85
|
+
const auth = await renderScreen();
|
|
86
|
+
await enterEmail();
|
|
87
|
+
await fireEvent.changeText(screen.getByTestId('sign-in-code'), '12345');
|
|
88
|
+
expect(auth.signInWithCode).not.toHaveBeenCalled();
|
|
89
|
+
await fireEvent.changeText(screen.getByTestId('sign-in-code'), '123 456');
|
|
90
|
+
expect(auth.signInWithCode).toHaveBeenCalledWith({ email: 'ana@example.com', otp: '123456' });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('a wrong code says so and clears the field', async () => {
|
|
94
|
+
const auth = await renderScreen({
|
|
95
|
+
auth: createAuth({ signInWithCode: jest.fn(async () => ({ status: 'invalid' })) }),
|
|
96
|
+
});
|
|
97
|
+
await enterEmail();
|
|
98
|
+
await fireEvent.changeText(screen.getByTestId('sign-in-code'), '000000');
|
|
99
|
+
expect(auth.signInWithCode).toHaveBeenCalledTimes(1);
|
|
100
|
+
expect(screen.getByTestId('sign-in-error')).toHaveTextContent(
|
|
101
|
+
'That code is not right, or it has expired.'
|
|
102
|
+
);
|
|
103
|
+
expect(screen.getByTestId('sign-in-code').props.value).toBe('');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('Resend waits 30 seconds, then sends another code', async () => {
|
|
107
|
+
jest.useFakeTimers();
|
|
108
|
+
const auth = await renderScreen();
|
|
109
|
+
await enterEmail();
|
|
110
|
+
expect(isDisabled('sign-in-resend')).toBe(true);
|
|
111
|
+
expect(screen.getByText('Resend in 30s')).toBeTruthy();
|
|
112
|
+
for (let second = 0; second < 29; second += 1) {
|
|
113
|
+
await act(async () => jest.advanceTimersByTime(1000));
|
|
114
|
+
}
|
|
115
|
+
expect(isDisabled('sign-in-resend')).toBe(true);
|
|
116
|
+
expect(screen.getByText('Resend in 1s')).toBeTruthy();
|
|
117
|
+
await act(async () => jest.advanceTimersByTime(1000));
|
|
118
|
+
expect(isDisabled('sign-in-resend')).toBe(false);
|
|
119
|
+
await fireEvent.press(screen.getByTestId('sign-in-resend'));
|
|
120
|
+
expect(auth.sendCode).toHaveBeenCalledTimes(2);
|
|
121
|
+
expect(isDisabled('sign-in-resend')).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('Change email goes back to the email field', async () => {
|
|
125
|
+
await renderScreen();
|
|
126
|
+
await enterEmail();
|
|
127
|
+
await fireEvent.press(screen.getByTestId('sign-in-change-email'));
|
|
128
|
+
expect(screen.getByTestId('sign-in-email').props.value).toBe(' ana@example.com ');
|
|
129
|
+
expect(screen.queryByTestId('sign-in-code')).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('a person with a second factor finishes in the sheet', async () => {
|
|
133
|
+
const auth = await renderScreen({
|
|
134
|
+
auth: createAuth({ signInWithCode: jest.fn(async () => ({ status: 'twoFactor' })) }),
|
|
135
|
+
});
|
|
136
|
+
await enterEmail();
|
|
137
|
+
await fireEvent.changeText(screen.getByTestId('sign-in-code'), '123456');
|
|
138
|
+
expect(auth.signInWithSheet).toHaveBeenCalledTimes(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('Other ways to sign in opens the sheet, and says so when it did not finish', async () => {
|
|
142
|
+
const auth = await renderScreen({
|
|
143
|
+
auth: createAuth({ signInWithSheet: jest.fn(async () => ({ status: 'failed' })) }),
|
|
144
|
+
});
|
|
145
|
+
await fireEvent.press(screen.getByTestId('sign-in-other'));
|
|
146
|
+
expect(auth.signInWithSheet).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(screen.getByTestId('sign-in-error')).toHaveTextContent(
|
|
148
|
+
'Sign-in did not finish. Try again.'
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('an app without email codes opens the sheet at once, with a button to open it again', async () => {
|
|
153
|
+
const auth = await renderScreen({ emailCode: false });
|
|
154
|
+
expect(auth.signInWithSheet).toHaveBeenCalledTimes(1);
|
|
155
|
+
expect(screen.queryByTestId('sign-in-email')).toBeNull();
|
|
156
|
+
await fireEvent.press(screen.getByTestId('sign-in-sheet'));
|
|
157
|
+
expect(auth.signInWithSheet).toHaveBeenCalledTimes(2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('a server without the email code route sends the person to the sheet', async () => {
|
|
161
|
+
const auth = await renderScreen({
|
|
162
|
+
auth: createAuth({ sendCode: jest.fn(async () => ({ status: 'unavailable' })) }),
|
|
163
|
+
});
|
|
164
|
+
await enterEmail();
|
|
165
|
+
expect(auth.signInWithSheet).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('too many codes, or no connection, say so', async () => {
|
|
169
|
+
const sendCode = jest
|
|
170
|
+
.fn()
|
|
171
|
+
.mockResolvedValueOnce({ status: 'rateLimited' })
|
|
172
|
+
.mockRejectedValueOnce(new TypeError('Network request failed'));
|
|
173
|
+
await renderScreen({ auth: createAuth({ sendCode }) });
|
|
174
|
+
await enterEmail();
|
|
175
|
+
expect(screen.getByTestId('sign-in-error')).toHaveTextContent(
|
|
176
|
+
'Too many tries. Wait a minute, then try again.'
|
|
177
|
+
);
|
|
178
|
+
await fireEvent.press(screen.getByTestId('sign-in-continue'));
|
|
179
|
+
expect(screen.getByTestId('sign-in-error')).toHaveTextContent(
|
|
180
|
+
"Could not reach the app's server. Check your connection and try again."
|
|
181
|
+
);
|
|
182
|
+
});
|