@entropy-softworks/ui 2026.7.31 → 2026.8.1
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/CHANGELOG.md +15 -0
- package/lib/components/button.d.ts +11 -2
- package/lib/components/button.d.ts.map +1 -1
- package/lib/components/button.jsx +14 -4
- package/lib/components/button.jsx.map +1 -1
- package/lib/context/AppConfigContext.d.ts +19 -0
- package/lib/context/AppConfigContext.d.ts.map +1 -1
- package/lib/context/AppConfigContext.jsx.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/screens/auth/AccountPickerScreen.d.ts.map +1 -1
- package/lib/screens/auth/AccountPickerScreen.jsx +3 -2
- package/lib/screens/auth/AccountPickerScreen.jsx.map +1 -1
- package/lib/screens/auth/AuthScreen.d.ts +45 -1
- package/lib/screens/auth/AuthScreen.d.ts.map +1 -1
- package/lib/screens/auth/AuthScreen.jsx +69 -27
- package/lib/screens/auth/AuthScreen.jsx.map +1 -1
- package/lib/screens/auth/ConsentScreen.d.ts.map +1 -1
- package/lib/screens/auth/ConsentScreen.jsx +2 -2
- package/lib/screens/auth/ConsentScreen.jsx.map +1 -1
- package/lib/screens/auth/ErrorScreen.d.ts.map +1 -1
- package/lib/screens/auth/ErrorScreen.jsx +2 -2
- package/lib/screens/auth/ErrorScreen.jsx.map +1 -1
- package/lib/screens/auth/ExpiredLinkScreen.d.ts.map +1 -1
- package/lib/screens/auth/ExpiredLinkScreen.jsx +2 -2
- package/lib/screens/auth/ExpiredLinkScreen.jsx.map +1 -1
- package/lib/screens/auth/ForgotPasswordScreen.d.ts.map +1 -1
- package/lib/screens/auth/ForgotPasswordScreen.jsx +7 -6
- package/lib/screens/auth/ForgotPasswordScreen.jsx.map +1 -1
- package/lib/screens/auth/ImportDataScreen.d.ts.map +1 -1
- package/lib/screens/auth/ImportDataScreen.jsx +1 -1
- package/lib/screens/auth/ImportDataScreen.jsx.map +1 -1
- package/lib/screens/auth/LoginScreen.d.ts.map +1 -1
- package/lib/screens/auth/LoginScreen.jsx +7 -6
- package/lib/screens/auth/LoginScreen.jsx.map +1 -1
- package/lib/screens/auth/MfaChallengeScreen.d.ts.map +1 -1
- package/lib/screens/auth/MfaChallengeScreen.jsx +3 -2
- package/lib/screens/auth/MfaChallengeScreen.jsx.map +1 -1
- package/lib/screens/auth/MfaEnrollScreen.d.ts.map +1 -1
- package/lib/screens/auth/MfaEnrollScreen.jsx +9 -8
- package/lib/screens/auth/MfaEnrollScreen.jsx.map +1 -1
- package/lib/screens/auth/PasskeyEnrollScreen.d.ts.map +1 -1
- package/lib/screens/auth/PasskeyEnrollScreen.jsx +2 -2
- package/lib/screens/auth/PasskeyEnrollScreen.jsx.map +1 -1
- package/lib/screens/auth/PrivacyPolicyScreen.d.ts.map +1 -1
- package/lib/screens/auth/PrivacyPolicyScreen.jsx +3 -2
- package/lib/screens/auth/PrivacyPolicyScreen.jsx.map +1 -1
- package/lib/screens/auth/RegisterScreen.d.ts.map +1 -1
- package/lib/screens/auth/RegisterScreen.jsx +6 -5
- package/lib/screens/auth/RegisterScreen.jsx.map +1 -1
- package/lib/screens/auth/ResetPasswordScreen.d.ts.map +1 -1
- package/lib/screens/auth/ResetPasswordScreen.jsx +6 -5
- package/lib/screens/auth/ResetPasswordScreen.jsx.map +1 -1
- package/lib/screens/auth/ServerConfigScreen.d.ts.map +1 -1
- package/lib/screens/auth/ServerConfigScreen.jsx +6 -5
- package/lib/screens/auth/ServerConfigScreen.jsx.map +1 -1
- package/lib/screens/auth/TermsOfServiceScreen.d.ts.map +1 -1
- package/lib/screens/auth/TermsOfServiceScreen.jsx +3 -2
- package/lib/screens/auth/TermsOfServiceScreen.jsx.map +1 -1
- package/lib/screens/auth/VerifyEmailScreen.d.ts.map +1 -1
- package/lib/screens/auth/VerifyEmailScreen.jsx +5 -4
- package/lib/screens/auth/VerifyEmailScreen.jsx.map +1 -1
- package/lib/screens/auth/WelcomeScreen.d.ts.map +1 -1
- package/lib/screens/auth/WelcomeScreen.jsx +2 -2
- package/lib/screens/auth/WelcomeScreen.jsx.map +1 -1
- package/lib/services/auth.service.d.ts +9 -0
- package/lib/services/auth.service.d.ts.map +1 -1
- package/lib/services/auth.service.js.map +1 -1
- package/lib/services/index.d.ts +1 -0
- package/lib/services/index.d.ts.map +1 -1
- package/lib/services/index.js +2 -0
- package/lib/services/index.js.map +1 -1
- package/lib/services/telemetry.d.ts +30 -0
- package/lib/services/telemetry.d.ts.map +1 -0
- package/lib/services/telemetry.js +72 -0
- package/lib/services/telemetry.js.map +1 -0
- package/package.json +1 -1
- package/src/components/button.tsx +32 -1
- package/src/context/AppConfigContext.tsx +19 -0
- package/src/index.ts +9 -0
- package/src/screens/auth/AccountPickerScreen.tsx +4 -2
- package/src/screens/auth/AuthScreen.tsx +159 -31
- package/src/screens/auth/ConsentScreen.tsx +4 -2
- package/src/screens/auth/ErrorScreen.tsx +4 -2
- package/src/screens/auth/ExpiredLinkScreen.tsx +4 -2
- package/src/screens/auth/ForgotPasswordScreen.tsx +10 -6
- package/src/screens/auth/ImportDataScreen.tsx +2 -1
- package/src/screens/auth/LoginScreen.tsx +10 -6
- package/src/screens/auth/MfaChallengeScreen.tsx +3 -2
- package/src/screens/auth/MfaEnrollScreen.tsx +15 -8
- package/src/screens/auth/PasskeyEnrollScreen.tsx +4 -2
- package/src/screens/auth/PrivacyPolicyScreen.tsx +4 -2
- package/src/screens/auth/RegisterScreen.tsx +8 -5
- package/src/screens/auth/ResetPasswordScreen.tsx +9 -5
- package/src/screens/auth/ServerConfigScreen.tsx +6 -5
- package/src/screens/auth/TermsOfServiceScreen.tsx +4 -2
- package/src/screens/auth/VerifyEmailScreen.tsx +8 -4
- package/src/screens/auth/WelcomeScreen.tsx +4 -2
- package/src/services/__tests__/telemetry.test.ts +81 -0
- package/src/services/auth.service.ts +9 -0
- package/src/services/index.ts +9 -0
- package/src/services/telemetry.ts +107 -0
|
@@ -11,6 +11,7 @@ import Animated, {
|
|
|
11
11
|
useSharedValue,
|
|
12
12
|
withTiming,
|
|
13
13
|
} from "react-native-reanimated";
|
|
14
|
+
import { withClickTracking } from "../../services/telemetry";
|
|
14
15
|
import { type Href } from "expo-router";
|
|
15
16
|
import { StatusBar } from "expo-status-bar";
|
|
16
17
|
import { Ionicons } from "@expo/vector-icons";
|
|
@@ -103,6 +104,50 @@ export interface AuthScreenProps {
|
|
|
103
104
|
decoration?: React.ReactNode;
|
|
104
105
|
/** Welcome-view tagline/subtitle. Defaults to AppConfig `tagline`. */
|
|
105
106
|
welcomeCtaLabel?: string;
|
|
107
|
+
/**
|
|
108
|
+
* Show the Sign Up half of the toggle. Defaults to `true`.
|
|
109
|
+
*
|
|
110
|
+
* Set `false` where the server has no self-service registration — an
|
|
111
|
+
* identity provider whose tenant is `closed`, for instance. The toggle is
|
|
112
|
+
* hidden rather than disabled, because a control that cannot ever succeed is
|
|
113
|
+
* worse than no control.
|
|
114
|
+
*/
|
|
115
|
+
signUpEnabled?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Collect an invite code on the Sign Up form and require it. For servers
|
|
118
|
+
* whose registration is invite-only.
|
|
119
|
+
*/
|
|
120
|
+
requireInviteCode?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Override registration entirely. When provided this runs instead of
|
|
123
|
+
* `useAuth().register`, and the screen shows `signUpSuccessMessage` rather
|
|
124
|
+
* than assuming the user is now signed in.
|
|
125
|
+
*
|
|
126
|
+
* This exists because registration is not uniform across the estate: the
|
|
127
|
+
* single-tenant apps sign you straight in, while the multi-tenant IdP creates
|
|
128
|
+
* an unverified account, emails a link, and leaves you signed out. Reshaping
|
|
129
|
+
* the shared `AuthService.register` to cover both would have changed the
|
|
130
|
+
* contract vault and simple-finance already depend on.
|
|
131
|
+
*/
|
|
132
|
+
onSignUp?: (input: {
|
|
133
|
+
email: string;
|
|
134
|
+
username: string;
|
|
135
|
+
password: string;
|
|
136
|
+
displayName: string;
|
|
137
|
+
inviteCode?: string;
|
|
138
|
+
}) => Promise<void>;
|
|
139
|
+
/** Shown after a successful `onSignUp`. */
|
|
140
|
+
signUpSuccessMessage?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Render the welcome stage (logo, decoration, "Get Started") before the form.
|
|
143
|
+
* Defaults to `true`.
|
|
144
|
+
*
|
|
145
|
+
* Set `false` to land straight on the form. An app whose only purpose at this
|
|
146
|
+
* URL is to authenticate has nothing to introduce, and a button that only
|
|
147
|
+
* reveals the form it precedes is pure friction. The in-card "← Back" is
|
|
148
|
+
* hidden too, since there is no stage behind it.
|
|
149
|
+
*/
|
|
150
|
+
showWelcome?: boolean;
|
|
106
151
|
}
|
|
107
152
|
|
|
108
153
|
/**
|
|
@@ -124,6 +169,11 @@ export function AuthScreen({
|
|
|
124
169
|
transition,
|
|
125
170
|
decoration,
|
|
126
171
|
welcomeCtaLabel = "Get Started",
|
|
172
|
+
signUpEnabled = true,
|
|
173
|
+
requireInviteCode = false,
|
|
174
|
+
onSignUp,
|
|
175
|
+
signUpSuccessMessage = "Account created. Check your email to verify the address, then sign in.",
|
|
176
|
+
showWelcome = true,
|
|
127
177
|
}: AuthScreenProps = {}) {
|
|
128
178
|
const {
|
|
129
179
|
login: authLogin,
|
|
@@ -132,21 +182,32 @@ export function AuthScreen({
|
|
|
132
182
|
unlockWithPassword,
|
|
133
183
|
} = useAuth();
|
|
134
184
|
const router = useNavigation();
|
|
135
|
-
const {
|
|
185
|
+
const {
|
|
186
|
+
appName,
|
|
187
|
+
Logo,
|
|
188
|
+
tagline,
|
|
189
|
+
routes,
|
|
190
|
+
termsText,
|
|
191
|
+
privacyText,
|
|
192
|
+
logoIsWordmark,
|
|
193
|
+
logoWidth,
|
|
194
|
+
} = useAppConfig();
|
|
136
195
|
|
|
137
196
|
const { colorScheme } = useColorScheme();
|
|
138
197
|
const isDark = colorScheme === "dark";
|
|
139
198
|
const t = useMemo(() => ({ ...DEFAULT_TRANSITION, ...transition }), [transition]);
|
|
140
199
|
|
|
141
200
|
const [mode, setMode] = useState<Mode>("signin");
|
|
142
|
-
const [
|
|
201
|
+
const [inviteCode, setInviteCode] = useState("");
|
|
202
|
+
const [signedUp, setSignedUp] = useState<string | null>(null);
|
|
203
|
+
const [stage, setStage] = useState<Stage>(showWelcome ? "welcome" : "auth");
|
|
143
204
|
const isSignup = mode === "signup";
|
|
144
205
|
|
|
145
206
|
// Stage morph: `stage` is the target; `displayStage` is what's rendered.
|
|
146
207
|
// On a stage change we fade/scale the content out, swap displayStage at zero
|
|
147
208
|
// opacity (so the centered-welcome → top-anchored-form layout change is
|
|
148
209
|
// invisible — no teleport, no overlap of the two states), then fade in.
|
|
149
|
-
const [displayStage, setDisplayStage] = useState<Stage>("welcome");
|
|
210
|
+
const [displayStage, setDisplayStage] = useState<Stage>(showWelcome ? "welcome" : "auth");
|
|
150
211
|
// Fade-through: `stage` is the target, `displayStage` is what's rendered. On
|
|
151
212
|
// a change the content fades/scales out, swaps at zero opacity (so the
|
|
152
213
|
// position change between stages is never seen), then fades back in.
|
|
@@ -182,6 +243,7 @@ export function AuthScreen({
|
|
|
182
243
|
displayName?: string;
|
|
183
244
|
password?: string;
|
|
184
245
|
confirmPassword?: string;
|
|
246
|
+
inviteCode?: string;
|
|
185
247
|
}>({});
|
|
186
248
|
|
|
187
249
|
const [legal, setLegal] = useState<null | "terms" | "privacy">(null);
|
|
@@ -269,6 +331,9 @@ export function AuthScreen({
|
|
|
269
331
|
} else if (password !== confirmPassword) {
|
|
270
332
|
next.confirmPassword = "Passwords do not match";
|
|
271
333
|
}
|
|
334
|
+
if (requireInviteCode && !inviteCode.trim()) {
|
|
335
|
+
next.inviteCode = "An invite code is required to create an account here";
|
|
336
|
+
}
|
|
272
337
|
}
|
|
273
338
|
return next;
|
|
274
339
|
};
|
|
@@ -334,7 +399,25 @@ export function AuthScreen({
|
|
|
334
399
|
}
|
|
335
400
|
try {
|
|
336
401
|
const username = (email.split("@")[0] || "user").replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
|
|
337
|
-
|
|
402
|
+
if (onSignUp) {
|
|
403
|
+
// The host owns the contract. It may leave the user signed out (the
|
|
404
|
+
// IdP emails a verification link), so show the notice rather than
|
|
405
|
+
// assuming a session now exists.
|
|
406
|
+
await onSignUp({
|
|
407
|
+
email: email.trim().toLowerCase(),
|
|
408
|
+
username,
|
|
409
|
+
password,
|
|
410
|
+
displayName: displayName.trim(),
|
|
411
|
+
inviteCode: requireInviteCode ? inviteCode.trim() : undefined,
|
|
412
|
+
});
|
|
413
|
+
setSignedUp(signUpSuccessMessage);
|
|
414
|
+
setPassword("");
|
|
415
|
+
setConfirmPassword("");
|
|
416
|
+
setInviteCode("");
|
|
417
|
+
setMode("signin");
|
|
418
|
+
} else {
|
|
419
|
+
await authRegister(email.trim().toLowerCase(), username, password, displayName.trim());
|
|
420
|
+
}
|
|
338
421
|
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
339
422
|
} catch (error) {
|
|
340
423
|
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
@@ -495,7 +578,8 @@ export function AuthScreen({
|
|
|
495
578
|
<IconButton
|
|
496
579
|
variant="ghost"
|
|
497
580
|
size="sm"
|
|
498
|
-
|
|
581
|
+
telemetryId="auth.auth.settings_open"
|
|
582
|
+
onPress={() => setSettingsOpen(true)}
|
|
499
583
|
accessibilityLabel="Connection settings"
|
|
500
584
|
accessibilityRole="button"
|
|
501
585
|
>
|
|
@@ -522,10 +606,17 @@ export function AuthScreen({
|
|
|
522
606
|
]}
|
|
523
607
|
>
|
|
524
608
|
<View className="mb-6 items-center">
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
609
|
+
{logoIsWordmark ? (
|
|
610
|
+
// The mark carries the name; height is left to the
|
|
611
|
+
// component so a wide wordmark keeps its aspect ratio, and
|
|
612
|
+
// the ink colour comes from the already-resolved theme.
|
|
613
|
+
<Logo width={logoWidth ?? 160} color={isDark ? "#FFFFFF" : "#000000"} />
|
|
614
|
+
) : (
|
|
615
|
+
<View className="flex-row items-center">
|
|
616
|
+
<Logo width={72} height={72} style={{ marginRight: -2 }} />
|
|
617
|
+
<Heading className="text-4xl">{appName}</Heading>
|
|
618
|
+
</View>
|
|
619
|
+
)}
|
|
529
620
|
{tagline ? (
|
|
530
621
|
<Text className="mt-1 font-mono text-base text-muted-foreground">
|
|
531
622
|
{tagline}
|
|
@@ -541,7 +632,8 @@ export function AuthScreen({
|
|
|
541
632
|
<View style={{ width: "100%", maxWidth: 320 }}>
|
|
542
633
|
<Button
|
|
543
634
|
className="w-full"
|
|
544
|
-
|
|
635
|
+
telemetryId="auth.auth.stage"
|
|
636
|
+
onPress={() => setStage("auth")}
|
|
545
637
|
accessibilityLabel={welcomeCtaLabel}
|
|
546
638
|
accessibilityRole="button"
|
|
547
639
|
testID="welcome-get-started"
|
|
@@ -594,7 +686,8 @@ export function AuthScreen({
|
|
|
594
686
|
{forgotSent ? (
|
|
595
687
|
<Button
|
|
596
688
|
className="w-full"
|
|
597
|
-
|
|
689
|
+
telemetryId="auth.auth.go_auth"
|
|
690
|
+
onPress={goAuth}
|
|
598
691
|
accessibilityLabel="Back to sign in"
|
|
599
692
|
>
|
|
600
693
|
Back to Sign In
|
|
@@ -603,7 +696,8 @@ export function AuthScreen({
|
|
|
603
696
|
<>
|
|
604
697
|
<Button
|
|
605
698
|
className="w-full"
|
|
606
|
-
|
|
699
|
+
telemetryId="auth.auth.forgot"
|
|
700
|
+
onPress={handleForgot}
|
|
607
701
|
isLoading={isLoading}
|
|
608
702
|
accessibilityLabel="Send reset link"
|
|
609
703
|
>
|
|
@@ -611,7 +705,7 @@ export function AuthScreen({
|
|
|
611
705
|
</Button>
|
|
612
706
|
<Text
|
|
613
707
|
className="mt-1 text-center font-mono text-sm text-muted-foreground"
|
|
614
|
-
onPress={goAuth}
|
|
708
|
+
onPress={withClickTracking("auth.auth.go_auth_2", goAuth)}
|
|
615
709
|
accessibilityRole="button"
|
|
616
710
|
accessibilityLabel="Back to sign in"
|
|
617
711
|
>
|
|
@@ -624,14 +718,19 @@ export function AuthScreen({
|
|
|
624
718
|
) : (
|
|
625
719
|
<>
|
|
626
720
|
<CardHeader className="gap-4">
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
{
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
721
|
+
{signUpEnabled ? (
|
|
722
|
+
<SegmentedToggle<Mode>
|
|
723
|
+
value={mode}
|
|
724
|
+
onChange={switchMode}
|
|
725
|
+
options={[
|
|
726
|
+
{ key: "signin", label: "Sign In" },
|
|
727
|
+
{ key: "signup", label: "Sign Up" },
|
|
728
|
+
]}
|
|
729
|
+
/>
|
|
730
|
+
) : null}
|
|
731
|
+
{signedUp ? (
|
|
732
|
+
<MutedText className="text-sm leading-5">{signedUp}</MutedText>
|
|
733
|
+
) : null}
|
|
635
734
|
<CardTitle>
|
|
636
735
|
{isSignup ? "Create your account" : `Sign in to ${appName}`}
|
|
637
736
|
</CardTitle>
|
|
@@ -649,7 +748,8 @@ export function AuthScreen({
|
|
|
649
748
|
<Button
|
|
650
749
|
variant="outline"
|
|
651
750
|
className="w-full"
|
|
652
|
-
|
|
751
|
+
telemetryId="auth.auth.passkey_login"
|
|
752
|
+
onPress={handlePasskeyLogin}
|
|
653
753
|
isLoading={passkeyStep === "passkey-loading"}
|
|
654
754
|
accessibilityLabel="Sign in with passkey"
|
|
655
755
|
accessibilityRole="button"
|
|
@@ -777,6 +877,28 @@ export function AuthScreen({
|
|
|
777
877
|
testID="auth-confirm-password-input"
|
|
778
878
|
/>
|
|
779
879
|
</ShakeField>
|
|
880
|
+
{requireInviteCode ? (
|
|
881
|
+
<View className="mt-3">
|
|
882
|
+
<ShakeField shake={confirmShake} label="Invite code">
|
|
883
|
+
<Input
|
|
884
|
+
value={inviteCode}
|
|
885
|
+
onChangeText={(text) => {
|
|
886
|
+
setInviteCode(text);
|
|
887
|
+
clearField("inviteCode");
|
|
888
|
+
}}
|
|
889
|
+
placeholder="Paste your invite code"
|
|
890
|
+
autoCapitalize="none"
|
|
891
|
+
autoCorrect={false}
|
|
892
|
+
returnKeyType="done"
|
|
893
|
+
onSubmitEditing={handleSubmit}
|
|
894
|
+
invalid={!!errors.inviteCode}
|
|
895
|
+
accessibilityLabel="Invite code"
|
|
896
|
+
testID="auth-invite-code-input"
|
|
897
|
+
/>
|
|
898
|
+
</ShakeField>
|
|
899
|
+
<FormError message={errors.inviteCode} />
|
|
900
|
+
</View>
|
|
901
|
+
) : null}
|
|
780
902
|
</Animated.View>
|
|
781
903
|
) : (
|
|
782
904
|
<Animated.View
|
|
@@ -788,11 +910,11 @@ export function AuthScreen({
|
|
|
788
910
|
>
|
|
789
911
|
<Text
|
|
790
912
|
className="font-mono text-sm underline"
|
|
791
|
-
onPress={() => {
|
|
913
|
+
onPress={withClickTracking("auth.auth.stage_2", () => {
|
|
792
914
|
setStage("forgot");
|
|
793
915
|
setForgotSent(false);
|
|
794
916
|
setErrors({});
|
|
795
|
-
}}
|
|
917
|
+
})}
|
|
796
918
|
accessibilityRole="link"
|
|
797
919
|
accessibilityLabel="Forgot password"
|
|
798
920
|
>
|
|
@@ -807,7 +929,8 @@ export function AuthScreen({
|
|
|
807
929
|
|
|
808
930
|
<Button
|
|
809
931
|
className="w-full"
|
|
810
|
-
|
|
932
|
+
telemetryId="auth.auth.submit"
|
|
933
|
+
onPress={handleSubmit}
|
|
811
934
|
isLoading={isLoading}
|
|
812
935
|
accessibilityLabel={isSignup ? "Create account" : "Sign in"}
|
|
813
936
|
accessibilityRole="button"
|
|
@@ -824,10 +947,15 @@ export function AuthScreen({
|
|
|
824
947
|
) : null}
|
|
825
948
|
|
|
826
949
|
{/* In-card back — thumb-reachable, so users don't
|
|
827
|
-
have to stretch to the top-left corner arrow.
|
|
950
|
+
have to stretch to the top-left corner arrow.
|
|
951
|
+
Hidden when there is no welcome stage behind it. */}
|
|
828
952
|
<Text
|
|
829
|
-
className=
|
|
830
|
-
|
|
953
|
+
className={
|
|
954
|
+
showWelcome
|
|
955
|
+
? "mt-1 text-center font-mono text-sm text-muted-foreground"
|
|
956
|
+
: "hidden"
|
|
957
|
+
}
|
|
958
|
+
onPress={withClickTracking("auth.auth.stage_3", () => setStage("welcome"))}
|
|
831
959
|
accessibilityRole="button"
|
|
832
960
|
accessibilityLabel="Back"
|
|
833
961
|
>
|
|
@@ -848,7 +976,7 @@ export function AuthScreen({
|
|
|
848
976
|
By continuing, you agree to our{" "}
|
|
849
977
|
<Text
|
|
850
978
|
className="text-xs text-foreground underline"
|
|
851
|
-
onPress={() => setLegal("terms")}
|
|
979
|
+
onPress={withClickTracking("auth.auth.legal", () => setLegal("terms"))}
|
|
852
980
|
accessibilityRole="link"
|
|
853
981
|
accessibilityLabel="Terms of Service"
|
|
854
982
|
>
|
|
@@ -857,7 +985,7 @@ export function AuthScreen({
|
|
|
857
985
|
and{" "}
|
|
858
986
|
<Text
|
|
859
987
|
className="text-xs text-foreground underline"
|
|
860
|
-
onPress={() => setLegal("privacy")}
|
|
988
|
+
onPress={withClickTracking("auth.auth.legal_2", () => setLegal("privacy"))}
|
|
861
989
|
accessibilityRole="link"
|
|
862
990
|
accessibilityLabel="Privacy Policy"
|
|
863
991
|
>
|
|
@@ -909,7 +1037,7 @@ export function AuthScreen({
|
|
|
909
1037
|
<DialogFooter style={{ justifyContent: "center" }}>
|
|
910
1038
|
<DialogButton
|
|
911
1039
|
variant="primary"
|
|
912
|
-
onPress={isLoading ? () => {} : handleUnlockAfterPasskey}
|
|
1040
|
+
onPress={withClickTracking("auth.auth.action", isLoading ? () => {} : handleUnlockAfterPasskey)}
|
|
913
1041
|
accessibilityLabel="Unlock"
|
|
914
1042
|
accessibilityRole="button"
|
|
915
1043
|
>
|
|
@@ -92,7 +92,8 @@ export function ConsentScreen({
|
|
|
92
92
|
<CardFooter className="flex-col gap-3 border-t-0">
|
|
93
93
|
<Button
|
|
94
94
|
className="w-full"
|
|
95
|
-
|
|
95
|
+
telemetryId="auth.consent.allow"
|
|
96
|
+
onPress={onAllow}
|
|
96
97
|
isLoading={isLoading}
|
|
97
98
|
accessibilityLabel={`Allow ${clientName}`}
|
|
98
99
|
accessibilityRole="button"
|
|
@@ -102,7 +103,8 @@ export function ConsentScreen({
|
|
|
102
103
|
<Button
|
|
103
104
|
className="w-full"
|
|
104
105
|
variant="outline"
|
|
105
|
-
|
|
106
|
+
telemetryId="auth.consent.deny"
|
|
107
|
+
onPress={onDeny}
|
|
106
108
|
accessibilityLabel="Deny access"
|
|
107
109
|
accessibilityRole="button"
|
|
108
110
|
>
|
|
@@ -67,7 +67,8 @@ export function ErrorScreen({
|
|
|
67
67
|
{onRetry ? (
|
|
68
68
|
<Button
|
|
69
69
|
className="w-full"
|
|
70
|
-
|
|
70
|
+
telemetryId="auth.error.retry"
|
|
71
|
+
onPress={onRetry}
|
|
71
72
|
accessibilityLabel="Try again"
|
|
72
73
|
accessibilityRole="button"
|
|
73
74
|
>
|
|
@@ -78,7 +79,8 @@ export function ErrorScreen({
|
|
|
78
79
|
<Button
|
|
79
80
|
className="w-full"
|
|
80
81
|
variant="outline"
|
|
81
|
-
|
|
82
|
+
telemetryId="auth.error.home"
|
|
83
|
+
onPress={onHome}
|
|
82
84
|
accessibilityLabel={homeLabel}
|
|
83
85
|
accessibilityRole="button"
|
|
84
86
|
>
|
|
@@ -58,7 +58,8 @@ export function ExpiredLinkScreen({
|
|
|
58
58
|
{onRequestNew ? (
|
|
59
59
|
<Button
|
|
60
60
|
className="w-full"
|
|
61
|
-
|
|
61
|
+
telemetryId="auth.expired_link.request_new"
|
|
62
|
+
onPress={onRequestNew}
|
|
62
63
|
accessibilityLabel={requestLabel}
|
|
63
64
|
accessibilityRole="button"
|
|
64
65
|
>
|
|
@@ -69,7 +70,8 @@ export function ExpiredLinkScreen({
|
|
|
69
70
|
<Button
|
|
70
71
|
className="w-full"
|
|
71
72
|
variant="outline"
|
|
72
|
-
|
|
73
|
+
telemetryId="auth.expired_link.home"
|
|
74
|
+
onPress={onHome}
|
|
73
75
|
accessibilityLabel={homeLabel}
|
|
74
76
|
accessibilityRole="button"
|
|
75
77
|
>
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
CardHeader,
|
|
18
18
|
CardTitle,
|
|
19
19
|
} from "../../components/card";
|
|
20
|
+
import { withClickTracking } from "../../services/telemetry";
|
|
20
21
|
import { EmailInput } from "../../components/input";
|
|
21
22
|
import AuthService from "../../services/auth.service";
|
|
22
23
|
import { validateEmail } from "../../utils/validation";
|
|
@@ -79,7 +80,8 @@ export function ForgotPasswordScreen() {
|
|
|
79
80
|
<View className="mt-4 self-start absolute top-4 left-6">
|
|
80
81
|
<IconButton
|
|
81
82
|
variant="ghost"
|
|
82
|
-
|
|
83
|
+
telemetryId="auth.forgot_password.back"
|
|
84
|
+
onPress={() => router.back()}
|
|
83
85
|
accessibilityLabel="Go back"
|
|
84
86
|
accessibilityRole="button"
|
|
85
87
|
>
|
|
@@ -120,7 +122,8 @@ export function ForgotPasswordScreen() {
|
|
|
120
122
|
<CardFooter className="flex-col gap-3 border-t-0">
|
|
121
123
|
<Button
|
|
122
124
|
className="w-full"
|
|
123
|
-
|
|
125
|
+
telemetryId="auth.forgot_password.submit"
|
|
126
|
+
onPress={handleSubmit}
|
|
124
127
|
isLoading={isLoading}
|
|
125
128
|
accessibilityLabel="Reset your password"
|
|
126
129
|
accessibilityRole="button"
|
|
@@ -132,7 +135,7 @@ export function ForgotPasswordScreen() {
|
|
|
132
135
|
Remember your password?{" "}
|
|
133
136
|
<Text
|
|
134
137
|
className="text-sm text-foreground underline"
|
|
135
|
-
onPress={() => router.push(routes.login)}
|
|
138
|
+
onPress={withClickTracking("auth.forgot_password.push", () => router.push(routes.login))}
|
|
136
139
|
accessibilityRole="link"
|
|
137
140
|
accessibilityLabel="Sign in"
|
|
138
141
|
>
|
|
@@ -145,7 +148,8 @@ export function ForgotPasswordScreen() {
|
|
|
145
148
|
<CardFooter className="flex-col gap-3 border-t-0">
|
|
146
149
|
<Button
|
|
147
150
|
className="w-full"
|
|
148
|
-
|
|
151
|
+
telemetryId="auth.forgot_password.push_2"
|
|
152
|
+
onPress={() =>
|
|
149
153
|
router.push({
|
|
150
154
|
pathname: routes.resetPassword,
|
|
151
155
|
params: { email },
|
|
@@ -161,10 +165,10 @@ export function ForgotPasswordScreen() {
|
|
|
161
165
|
Didn't receive the code?{" "}
|
|
162
166
|
<Text
|
|
163
167
|
className="text-sm text-foreground underline"
|
|
164
|
-
onPress={() => {
|
|
168
|
+
onPress={withClickTracking("auth.forgot_password.sent", () => {
|
|
165
169
|
setSent(false);
|
|
166
170
|
setError("");
|
|
167
|
-
}}
|
|
171
|
+
})}
|
|
168
172
|
accessibilityRole="button"
|
|
169
173
|
accessibilityLabel="Resend reset email"
|
|
170
174
|
>
|
|
@@ -95,7 +95,8 @@ export function ImportDataScreen({ renderImport }: ImportDataScreenProps = {}) {
|
|
|
95
95
|
<CardFooter className="border-t-0">
|
|
96
96
|
<Button
|
|
97
97
|
className="w-full"
|
|
98
|
-
|
|
98
|
+
telemetryId="auth.import_data.complete_and_navigate"
|
|
99
|
+
onPress={completeAndNavigate}
|
|
99
100
|
accessibilityLabel={`Continue to ${appName}`}
|
|
100
101
|
accessibilityRole="button"
|
|
101
102
|
testID="import-continue"
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
CardHeader,
|
|
18
18
|
CardTitle,
|
|
19
19
|
} from "../../components/card";
|
|
20
|
+
import { withClickTracking } from "../../services/telemetry";
|
|
20
21
|
import { EmailInput, PasswordInput } from "../../components/input";
|
|
21
22
|
import { Dialog, DialogButton, DialogFooter } from "../../components/dialog";
|
|
22
23
|
import { SeparatorWithText } from "../../components/separator";
|
|
@@ -280,7 +281,8 @@ export function LoginScreen({
|
|
|
280
281
|
<View className="mt-4">
|
|
281
282
|
<IconButton
|
|
282
283
|
variant="ghost"
|
|
283
|
-
|
|
284
|
+
telemetryId="auth.login.push"
|
|
285
|
+
onPress={() => router.push(routes.welcome)}
|
|
284
286
|
className="self-start"
|
|
285
287
|
accessibilityLabel="Go back"
|
|
286
288
|
accessibilityRole="button"
|
|
@@ -315,7 +317,8 @@ export function LoginScreen({
|
|
|
315
317
|
<Button
|
|
316
318
|
variant="outline"
|
|
317
319
|
className="w-full"
|
|
318
|
-
|
|
320
|
+
telemetryId="auth.login.passkey_login"
|
|
321
|
+
onPress={handlePasskeyLogin}
|
|
319
322
|
isLoading={passkeyStep === "passkey-loading"}
|
|
320
323
|
accessibilityLabel="Sign in with passkey"
|
|
321
324
|
accessibilityRole="button"
|
|
@@ -375,7 +378,7 @@ export function LoginScreen({
|
|
|
375
378
|
<View className="items-end">
|
|
376
379
|
<Text
|
|
377
380
|
className="font-mono text-sm underline"
|
|
378
|
-
onPress={() => router.push(routes.forgotPassword)}
|
|
381
|
+
onPress={withClickTracking("auth.login.push_2", () => router.push(routes.forgotPassword))}
|
|
379
382
|
accessibilityRole="link"
|
|
380
383
|
accessibilityLabel="Forgot password"
|
|
381
384
|
>
|
|
@@ -389,7 +392,7 @@ export function LoginScreen({
|
|
|
389
392
|
Don't have an account?{" "}
|
|
390
393
|
<Text
|
|
391
394
|
className="text-sm text-foreground underline"
|
|
392
|
-
onPress={() => router.push(routes.register)}
|
|
395
|
+
onPress={withClickTracking("auth.login.push_3", () => router.push(routes.register))}
|
|
393
396
|
accessibilityRole="link"
|
|
394
397
|
accessibilityLabel="Sign up"
|
|
395
398
|
>
|
|
@@ -401,7 +404,8 @@ export function LoginScreen({
|
|
|
401
404
|
|
|
402
405
|
<Button
|
|
403
406
|
className="w-full"
|
|
404
|
-
|
|
407
|
+
telemetryId="auth.login.login"
|
|
408
|
+
onPress={handleLogin}
|
|
405
409
|
isLoading={isLoading}
|
|
406
410
|
accessibilityLabel="Sign in"
|
|
407
411
|
accessibilityRole="button"
|
|
@@ -469,7 +473,7 @@ export function LoginScreen({
|
|
|
469
473
|
<DialogFooter style={{ justifyContent: "center" }}>
|
|
470
474
|
<DialogButton
|
|
471
475
|
variant="primary"
|
|
472
|
-
onPress={isLoading ? () => {} : handleUnlockAfterPasskey}
|
|
476
|
+
onPress={withClickTracking("auth.login.action", isLoading ? () => {} : handleUnlockAfterPasskey)}
|
|
473
477
|
accessibilityLabel="Unlock"
|
|
474
478
|
accessibilityRole="button"
|
|
475
479
|
>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withClickTracking } from "../../services/telemetry";
|
|
1
2
|
import React, { useState } from "react";
|
|
2
3
|
import { ActivityIndicator, Platform, Pressable, Text, TextInput, View } from "react-native";
|
|
3
4
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
@@ -169,7 +170,7 @@ export function MfaChallengeScreen() {
|
|
|
169
170
|
)}
|
|
170
171
|
|
|
171
172
|
<Pressable
|
|
172
|
-
onPress={handleVerify}
|
|
173
|
+
onPress={withClickTracking("auth.mfa_challenge.verify", handleVerify)}
|
|
173
174
|
disabled={isLoading}
|
|
174
175
|
className="rounded-lg items-center mt-5"
|
|
175
176
|
style={({ pressed }) => ({
|
|
@@ -194,7 +195,7 @@ export function MfaChallengeScreen() {
|
|
|
194
195
|
</Pressable>
|
|
195
196
|
|
|
196
197
|
<Pressable
|
|
197
|
-
onPress={toggleRecoveryMode}
|
|
198
|
+
onPress={withClickTracking("auth.mfa_challenge.toggle_recovery_mode", toggleRecoveryMode)}
|
|
198
199
|
className="mt-5 items-center"
|
|
199
200
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
200
201
|
accessibilityRole="button"
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
CardHeader,
|
|
20
20
|
CardTitle,
|
|
21
21
|
} from "../../components/card";
|
|
22
|
+
import { withClickTracking } from "../../services/telemetry";
|
|
22
23
|
import { Input } from "../../components/input";
|
|
23
24
|
import { Separator } from "../../components/separator";
|
|
24
25
|
import AuthService from "../../services/auth.service";
|
|
@@ -191,7 +192,8 @@ export function MfaEnrollScreen({ authenticatorAppPromo }: MfaEnrollScreenProps
|
|
|
191
192
|
<CardFooter className="flex-col gap-3 border-t-0">
|
|
192
193
|
<Button
|
|
193
194
|
className="w-full"
|
|
194
|
-
|
|
195
|
+
telemetryId="auth.mfa_enroll.start_setup"
|
|
196
|
+
onPress={handleStartSetup}
|
|
195
197
|
isLoading={isLoading}
|
|
196
198
|
accessibilityLabel="Enable MFA"
|
|
197
199
|
accessibilityRole="button"
|
|
@@ -201,7 +203,8 @@ export function MfaEnrollScreen({ authenticatorAppPromo }: MfaEnrollScreenProps
|
|
|
201
203
|
<Button
|
|
202
204
|
variant="ghost"
|
|
203
205
|
className="w-full"
|
|
204
|
-
|
|
206
|
+
telemetryId="auth.mfa_enroll.skip"
|
|
207
|
+
onPress={handleSkip}
|
|
205
208
|
accessibilityLabel="Skip for now"
|
|
206
209
|
accessibilityRole="button"
|
|
207
210
|
>
|
|
@@ -243,7 +246,7 @@ export function MfaEnrollScreen({ authenticatorAppPromo }: MfaEnrollScreenProps
|
|
|
243
246
|
Can't scan? Enter the secret key manually
|
|
244
247
|
</MutedText>
|
|
245
248
|
<Pressable
|
|
246
|
-
onPress={() => copyToClipboard(setupData.secret, "Secret")}
|
|
249
|
+
onPress={withClickTracking("auth.mfa_enroll.copy_to_clipboard", () => copyToClipboard(setupData.secret, "Secret"))}
|
|
247
250
|
className="rounded-lg bg-secondary p-3 active:opacity-70 dark:bg-neutral-800"
|
|
248
251
|
accessibilityRole="button"
|
|
249
252
|
accessibilityLabel="Copy secret key"
|
|
@@ -257,7 +260,7 @@ export function MfaEnrollScreen({ authenticatorAppPromo }: MfaEnrollScreenProps
|
|
|
257
260
|
</View>
|
|
258
261
|
|
|
259
262
|
<Pressable
|
|
260
|
-
onPress={() => copyToClipboard(setupData.uri, "URI")}
|
|
263
|
+
onPress={withClickTracking("auth.mfa_enroll.copy_to_clipboard_2", () => copyToClipboard(setupData.uri, "URI"))}
|
|
261
264
|
className="rounded-lg bg-secondary px-3 py-2 active:opacity-70 dark:bg-neutral-800"
|
|
262
265
|
accessibilityRole="button"
|
|
263
266
|
accessibilityLabel="Copy full OTP URI"
|
|
@@ -293,7 +296,8 @@ export function MfaEnrollScreen({ authenticatorAppPromo }: MfaEnrollScreenProps
|
|
|
293
296
|
<CardFooter className="flex-col gap-3 border-t-0">
|
|
294
297
|
<Button
|
|
295
298
|
className="w-full"
|
|
296
|
-
|
|
299
|
+
telemetryId="auth.mfa_enroll.verify_code"
|
|
300
|
+
onPress={handleVerifyCode}
|
|
297
301
|
isLoading={isLoading}
|
|
298
302
|
disabled={code.length !== 6}
|
|
299
303
|
accessibilityLabel="Verify and enable MFA"
|
|
@@ -304,7 +308,8 @@ export function MfaEnrollScreen({ authenticatorAppPromo }: MfaEnrollScreenProps
|
|
|
304
308
|
<Button
|
|
305
309
|
variant="ghost"
|
|
306
310
|
className="w-full"
|
|
307
|
-
|
|
311
|
+
telemetryId="auth.mfa_enroll.skip_2"
|
|
312
|
+
onPress={handleSkip}
|
|
308
313
|
accessibilityLabel="Skip for now"
|
|
309
314
|
accessibilityRole="button"
|
|
310
315
|
>
|
|
@@ -343,7 +348,8 @@ export function MfaEnrollScreen({ authenticatorAppPromo }: MfaEnrollScreenProps
|
|
|
343
348
|
<Button
|
|
344
349
|
variant="outline"
|
|
345
350
|
className="w-full"
|
|
346
|
-
|
|
351
|
+
telemetryId="auth.mfa_enroll.copy_to_clipboard_3"
|
|
352
|
+
onPress={() =>
|
|
347
353
|
copyToClipboard(setupData.recovery_codes.join("\n"), "Recovery codes")
|
|
348
354
|
}
|
|
349
355
|
accessibilityLabel="Copy all recovery codes"
|
|
@@ -370,7 +376,8 @@ export function MfaEnrollScreen({ authenticatorAppPromo }: MfaEnrollScreenProps
|
|
|
370
376
|
<CardFooter className="border-t-0">
|
|
371
377
|
<Button
|
|
372
378
|
className="w-full"
|
|
373
|
-
|
|
379
|
+
telemetryId="auth.mfa_enroll.finish"
|
|
380
|
+
onPress={handleFinish}
|
|
374
381
|
accessibilityLabel={`Continue to ${appName}`}
|
|
375
382
|
accessibilityRole="button"
|
|
376
383
|
>
|