@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/lib/components/button.d.ts +11 -2
  3. package/lib/components/button.d.ts.map +1 -1
  4. package/lib/components/button.jsx +14 -4
  5. package/lib/components/button.jsx.map +1 -1
  6. package/lib/context/AppConfigContext.d.ts +19 -0
  7. package/lib/context/AppConfigContext.d.ts.map +1 -1
  8. package/lib/context/AppConfigContext.jsx.map +1 -1
  9. package/lib/index.d.ts +1 -0
  10. package/lib/index.d.ts.map +1 -1
  11. package/lib/index.js +2 -0
  12. package/lib/index.js.map +1 -1
  13. package/lib/screens/auth/AccountPickerScreen.d.ts.map +1 -1
  14. package/lib/screens/auth/AccountPickerScreen.jsx +3 -2
  15. package/lib/screens/auth/AccountPickerScreen.jsx.map +1 -1
  16. package/lib/screens/auth/AuthScreen.d.ts +45 -1
  17. package/lib/screens/auth/AuthScreen.d.ts.map +1 -1
  18. package/lib/screens/auth/AuthScreen.jsx +69 -27
  19. package/lib/screens/auth/AuthScreen.jsx.map +1 -1
  20. package/lib/screens/auth/ConsentScreen.d.ts.map +1 -1
  21. package/lib/screens/auth/ConsentScreen.jsx +2 -2
  22. package/lib/screens/auth/ConsentScreen.jsx.map +1 -1
  23. package/lib/screens/auth/ErrorScreen.d.ts.map +1 -1
  24. package/lib/screens/auth/ErrorScreen.jsx +2 -2
  25. package/lib/screens/auth/ErrorScreen.jsx.map +1 -1
  26. package/lib/screens/auth/ExpiredLinkScreen.d.ts.map +1 -1
  27. package/lib/screens/auth/ExpiredLinkScreen.jsx +2 -2
  28. package/lib/screens/auth/ExpiredLinkScreen.jsx.map +1 -1
  29. package/lib/screens/auth/ForgotPasswordScreen.d.ts.map +1 -1
  30. package/lib/screens/auth/ForgotPasswordScreen.jsx +7 -6
  31. package/lib/screens/auth/ForgotPasswordScreen.jsx.map +1 -1
  32. package/lib/screens/auth/ImportDataScreen.d.ts.map +1 -1
  33. package/lib/screens/auth/ImportDataScreen.jsx +1 -1
  34. package/lib/screens/auth/ImportDataScreen.jsx.map +1 -1
  35. package/lib/screens/auth/LoginScreen.d.ts.map +1 -1
  36. package/lib/screens/auth/LoginScreen.jsx +7 -6
  37. package/lib/screens/auth/LoginScreen.jsx.map +1 -1
  38. package/lib/screens/auth/MfaChallengeScreen.d.ts.map +1 -1
  39. package/lib/screens/auth/MfaChallengeScreen.jsx +3 -2
  40. package/lib/screens/auth/MfaChallengeScreen.jsx.map +1 -1
  41. package/lib/screens/auth/MfaEnrollScreen.d.ts.map +1 -1
  42. package/lib/screens/auth/MfaEnrollScreen.jsx +9 -8
  43. package/lib/screens/auth/MfaEnrollScreen.jsx.map +1 -1
  44. package/lib/screens/auth/PasskeyEnrollScreen.d.ts.map +1 -1
  45. package/lib/screens/auth/PasskeyEnrollScreen.jsx +2 -2
  46. package/lib/screens/auth/PasskeyEnrollScreen.jsx.map +1 -1
  47. package/lib/screens/auth/PrivacyPolicyScreen.d.ts.map +1 -1
  48. package/lib/screens/auth/PrivacyPolicyScreen.jsx +3 -2
  49. package/lib/screens/auth/PrivacyPolicyScreen.jsx.map +1 -1
  50. package/lib/screens/auth/RegisterScreen.d.ts.map +1 -1
  51. package/lib/screens/auth/RegisterScreen.jsx +6 -5
  52. package/lib/screens/auth/RegisterScreen.jsx.map +1 -1
  53. package/lib/screens/auth/ResetPasswordScreen.d.ts.map +1 -1
  54. package/lib/screens/auth/ResetPasswordScreen.jsx +6 -5
  55. package/lib/screens/auth/ResetPasswordScreen.jsx.map +1 -1
  56. package/lib/screens/auth/ServerConfigScreen.d.ts.map +1 -1
  57. package/lib/screens/auth/ServerConfigScreen.jsx +6 -5
  58. package/lib/screens/auth/ServerConfigScreen.jsx.map +1 -1
  59. package/lib/screens/auth/TermsOfServiceScreen.d.ts.map +1 -1
  60. package/lib/screens/auth/TermsOfServiceScreen.jsx +3 -2
  61. package/lib/screens/auth/TermsOfServiceScreen.jsx.map +1 -1
  62. package/lib/screens/auth/VerifyEmailScreen.d.ts.map +1 -1
  63. package/lib/screens/auth/VerifyEmailScreen.jsx +5 -4
  64. package/lib/screens/auth/VerifyEmailScreen.jsx.map +1 -1
  65. package/lib/screens/auth/WelcomeScreen.d.ts.map +1 -1
  66. package/lib/screens/auth/WelcomeScreen.jsx +2 -2
  67. package/lib/screens/auth/WelcomeScreen.jsx.map +1 -1
  68. package/lib/services/auth.service.d.ts +9 -0
  69. package/lib/services/auth.service.d.ts.map +1 -1
  70. package/lib/services/auth.service.js.map +1 -1
  71. package/lib/services/index.d.ts +1 -0
  72. package/lib/services/index.d.ts.map +1 -1
  73. package/lib/services/index.js +2 -0
  74. package/lib/services/index.js.map +1 -1
  75. package/lib/services/telemetry.d.ts +30 -0
  76. package/lib/services/telemetry.d.ts.map +1 -0
  77. package/lib/services/telemetry.js +72 -0
  78. package/lib/services/telemetry.js.map +1 -0
  79. package/package.json +1 -1
  80. package/src/components/button.tsx +32 -1
  81. package/src/context/AppConfigContext.tsx +19 -0
  82. package/src/index.ts +9 -0
  83. package/src/screens/auth/AccountPickerScreen.tsx +4 -2
  84. package/src/screens/auth/AuthScreen.tsx +159 -31
  85. package/src/screens/auth/ConsentScreen.tsx +4 -2
  86. package/src/screens/auth/ErrorScreen.tsx +4 -2
  87. package/src/screens/auth/ExpiredLinkScreen.tsx +4 -2
  88. package/src/screens/auth/ForgotPasswordScreen.tsx +10 -6
  89. package/src/screens/auth/ImportDataScreen.tsx +2 -1
  90. package/src/screens/auth/LoginScreen.tsx +10 -6
  91. package/src/screens/auth/MfaChallengeScreen.tsx +3 -2
  92. package/src/screens/auth/MfaEnrollScreen.tsx +15 -8
  93. package/src/screens/auth/PasskeyEnrollScreen.tsx +4 -2
  94. package/src/screens/auth/PrivacyPolicyScreen.tsx +4 -2
  95. package/src/screens/auth/RegisterScreen.tsx +8 -5
  96. package/src/screens/auth/ResetPasswordScreen.tsx +9 -5
  97. package/src/screens/auth/ServerConfigScreen.tsx +6 -5
  98. package/src/screens/auth/TermsOfServiceScreen.tsx +4 -2
  99. package/src/screens/auth/VerifyEmailScreen.tsx +8 -4
  100. package/src/screens/auth/WelcomeScreen.tsx +4 -2
  101. package/src/services/__tests__/telemetry.test.ts +81 -0
  102. package/src/services/auth.service.ts +9 -0
  103. package/src/services/index.ts +9 -0
  104. 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 { appName, Logo, tagline, routes, termsText, privacyText } = useAppConfig();
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 [stage, setStage] = useState<Stage>("welcome");
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
- await authRegister(email.trim().toLowerCase(), username, password, displayName.trim());
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
- onPress={() => setSettingsOpen(true)}
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
- <View className="flex-row items-center">
526
- <Logo width={72} height={72} style={{ marginRight: -2 }} />
527
- <Heading className="text-4xl">{appName}</Heading>
528
- </View>
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
- onPress={() => setStage("auth")}
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
- onPress={goAuth}
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
- onPress={handleForgot}
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
- <SegmentedToggle<Mode>
628
- value={mode}
629
- onChange={switchMode}
630
- options={[
631
- { key: "signin", label: "Sign In" },
632
- { key: "signup", label: "Sign Up" },
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
- onPress={handlePasskeyLogin}
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
- onPress={handleSubmit}
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="mt-1 text-center font-mono text-sm text-muted-foreground"
830
- onPress={() => setStage("welcome")}
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
- onPress={onAllow}
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
- onPress={onDeny}
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
- onPress={onRetry}
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
- onPress={onHome}
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
- onPress={onRequestNew}
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
- onPress={onHome}
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
- onPress={() => router.back()}
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
- onPress={handleSubmit}
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
- onPress={() =>
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
- onPress={completeAndNavigate}
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
- onPress={() => router.push(routes.welcome)}
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
- onPress={handlePasskeyLogin}
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&apos;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
- onPress={handleLogin}
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
- onPress={handleStartSetup}
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
- onPress={handleSkip}
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&apos;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
- onPress={handleVerifyCode}
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
- onPress={handleSkip}
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
- onPress={() =>
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
- onPress={handleFinish}
379
+ telemetryId="auth.mfa_enroll.finish"
380
+ onPress={handleFinish}
374
381
  accessibilityLabel={`Continue to ${appName}`}
375
382
  accessibilityRole="button"
376
383
  >