@entropy-softworks/ui 2026.7.31 → 2026.8.2
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 +25 -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/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/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
|
@@ -70,7 +70,8 @@ export function PasskeyEnrollScreen({
|
|
|
70
70
|
<CardFooter className="flex-col gap-3 border-t-0">
|
|
71
71
|
<Button
|
|
72
72
|
className="w-full"
|
|
73
|
-
|
|
73
|
+
telemetryId="auth.passkey_enroll.enroll"
|
|
74
|
+
onPress={onEnroll}
|
|
74
75
|
isLoading={isLoading}
|
|
75
76
|
accessibilityLabel="Add a passkey"
|
|
76
77
|
accessibilityRole="button"
|
|
@@ -80,7 +81,8 @@ export function PasskeyEnrollScreen({
|
|
|
80
81
|
<Button
|
|
81
82
|
className="w-full"
|
|
82
83
|
variant="ghost"
|
|
83
|
-
|
|
84
|
+
telemetryId="auth.passkey_enroll.skip"
|
|
85
|
+
onPress={onSkip}
|
|
84
86
|
accessibilityLabel="Not now"
|
|
85
87
|
accessibilityRole="button"
|
|
86
88
|
>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withClickTracking } from "../../services/telemetry";
|
|
1
2
|
import React from "react";
|
|
2
3
|
import { Linking, ScrollView, Text, View } from "react-native";
|
|
3
4
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
@@ -61,7 +62,8 @@ export function PrivacyPolicyScreen({
|
|
|
61
62
|
<View className="flex-1" style={{ backgroundColor: isDark ? "#000000" : "#FFFFFF" }}>
|
|
62
63
|
<View className="px-6 pt-4 pb-6">
|
|
63
64
|
<View className="flex-row items-center justify-between mb-6">
|
|
64
|
-
<IconButton variant="ghost" size="sm"
|
|
65
|
+
<IconButton variant="ghost" size="sm" telemetryId="auth.privacy_policy.back"
|
|
66
|
+
onPress={() => router.back()}>
|
|
65
67
|
<Ionicons name="arrow-back" size={20} color={isDark ? "#FFFFFF" : "#000000"} />
|
|
66
68
|
</IconButton>
|
|
67
69
|
|
|
@@ -189,7 +191,7 @@ export function PrivacyPolicyScreen({
|
|
|
189
191
|
<Text
|
|
190
192
|
className="font-mono text-sm underline"
|
|
191
193
|
style={{ color: isDark ? "#6BA3E5" : "#4A90E2" }}
|
|
192
|
-
onPress={() => Linking.openURL(`mailto:${supportEmail}`)}
|
|
194
|
+
onPress={withClickTracking("auth.privacy_policy.open_u_r_l", () => Linking.openURL(`mailto:${supportEmail}`))}
|
|
193
195
|
>
|
|
194
196
|
{supportEmail}
|
|
195
197
|
</Text>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withClickTracking } from "../../services/telemetry";
|
|
1
2
|
import React, { useEffect, useMemo, useState } from "react";
|
|
2
3
|
import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
|
|
3
4
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
@@ -232,7 +233,8 @@ export function RegisterScreen({
|
|
|
232
233
|
<IconButton
|
|
233
234
|
variant="ghost"
|
|
234
235
|
size="sm"
|
|
235
|
-
|
|
236
|
+
telemetryId="auth.register.push"
|
|
237
|
+
onPress={() => router.push(routes.welcome)}
|
|
236
238
|
accessibilityLabel="Go back"
|
|
237
239
|
accessibilityRole="button"
|
|
238
240
|
>
|
|
@@ -361,7 +363,7 @@ export function RegisterScreen({
|
|
|
361
363
|
I agree to the{" "}
|
|
362
364
|
<Text
|
|
363
365
|
className="text-sm underline"
|
|
364
|
-
onPress={() => router.push(routes.termsOfService)}
|
|
366
|
+
onPress={withClickTracking("auth.register.push_2", () => router.push(routes.termsOfService))}
|
|
365
367
|
accessibilityRole="link"
|
|
366
368
|
accessibilityLabel="Terms of Service"
|
|
367
369
|
>
|
|
@@ -370,7 +372,7 @@ export function RegisterScreen({
|
|
|
370
372
|
and{" "}
|
|
371
373
|
<Text
|
|
372
374
|
className="text-sm underline"
|
|
373
|
-
onPress={() => router.push(routes.privacyPolicy)}
|
|
375
|
+
onPress={withClickTracking("auth.register.push_3", () => router.push(routes.privacyPolicy))}
|
|
374
376
|
accessibilityRole="link"
|
|
375
377
|
accessibilityLabel="Privacy Policy"
|
|
376
378
|
>
|
|
@@ -386,7 +388,7 @@ export function RegisterScreen({
|
|
|
386
388
|
Already have an account?{" "}
|
|
387
389
|
<Text
|
|
388
390
|
className="text-sm text-foreground underline"
|
|
389
|
-
onPress={() => router.push(routes.login)}
|
|
391
|
+
onPress={withClickTracking("auth.register.push_4", () => router.push(routes.login))}
|
|
390
392
|
accessibilityRole="link"
|
|
391
393
|
accessibilityLabel="Sign in"
|
|
392
394
|
>
|
|
@@ -399,7 +401,8 @@ export function RegisterScreen({
|
|
|
399
401
|
<Button
|
|
400
402
|
className="w-full"
|
|
401
403
|
isLoading={isLoading}
|
|
402
|
-
|
|
404
|
+
telemetryId="auth.register.register"
|
|
405
|
+
onPress={handleRegister}
|
|
403
406
|
disabled={Platform.OS === "web" && !email && !password}
|
|
404
407
|
accessibilityLabel="Create account"
|
|
405
408
|
accessibilityRole="button"
|
|
@@ -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 { Input, PasswordInput } from "../../components/input";
|
|
21
22
|
import PasswordStrengthMeter from "../../components/auth/PasswordStrengthMeter";
|
|
22
23
|
import PwnedPasswordWarning from "../../components/auth/PwnedPasswordWarning";
|
|
@@ -228,7 +229,8 @@ export function ResetPasswordScreen({ encryptionWarning }: ResetPasswordScreenPr
|
|
|
228
229
|
<View className="mt-4 self-start absolute top-4 left-6">
|
|
229
230
|
<IconButton
|
|
230
231
|
variant="ghost"
|
|
231
|
-
|
|
232
|
+
telemetryId="auth.reset_password.step"
|
|
233
|
+
onPress={() => (step === "password" ? setStep("code") : router.back())}
|
|
232
234
|
accessibilityLabel="Go back"
|
|
233
235
|
accessibilityRole="button"
|
|
234
236
|
>
|
|
@@ -270,7 +272,8 @@ export function ResetPasswordScreen({ encryptionWarning }: ResetPasswordScreenPr
|
|
|
270
272
|
|
|
271
273
|
<Button
|
|
272
274
|
className="w-full"
|
|
273
|
-
|
|
275
|
+
telemetryId="auth.reset_password.code_submit"
|
|
276
|
+
onPress={handleCodeSubmit}
|
|
274
277
|
isLoading={isLoading}
|
|
275
278
|
accessibilityLabel="Continue"
|
|
276
279
|
accessibilityRole="button"
|
|
@@ -282,7 +285,7 @@ export function ResetPasswordScreen({ encryptionWarning }: ResetPasswordScreenPr
|
|
|
282
285
|
Back to{" "}
|
|
283
286
|
<Text
|
|
284
287
|
className="text-sm text-foreground underline"
|
|
285
|
-
onPress={() => router.push(routes.login)}
|
|
288
|
+
onPress={withClickTracking("auth.reset_password.push", () => router.push(routes.login))}
|
|
286
289
|
accessibilityRole="link"
|
|
287
290
|
accessibilityLabel="Sign in"
|
|
288
291
|
>
|
|
@@ -347,7 +350,8 @@ export function ResetPasswordScreen({ encryptionWarning }: ResetPasswordScreenPr
|
|
|
347
350
|
|
|
348
351
|
<Button
|
|
349
352
|
className="w-full"
|
|
350
|
-
|
|
353
|
+
telemetryId="auth.reset_password.password_submit"
|
|
354
|
+
onPress={handlePasswordSubmit}
|
|
351
355
|
isLoading={isLoading}
|
|
352
356
|
accessibilityLabel="Reset password"
|
|
353
357
|
accessibilityRole="button"
|
|
@@ -358,7 +362,7 @@ export function ResetPasswordScreen({ encryptionWarning }: ResetPasswordScreenPr
|
|
|
358
362
|
<MutedText className="text-center text-sm">
|
|
359
363
|
<Text
|
|
360
364
|
className="text-sm text-foreground underline"
|
|
361
|
-
onPress={() => setStep("code")}
|
|
365
|
+
onPress={withClickTracking("auth.reset_password.step_2", () => setStep("code"))}
|
|
362
366
|
accessibilityRole="link"
|
|
363
367
|
accessibilityLabel="Use a different code"
|
|
364
368
|
>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
TextInput,
|
|
9
9
|
View,
|
|
10
10
|
} from "react-native";
|
|
11
|
+
import { withClickTracking } from "../../services/telemetry";
|
|
11
12
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
12
13
|
import { useNavigation } from "../../context/NavigationContext";
|
|
13
14
|
import { StatusBar } from "expo-status-bar";
|
|
@@ -243,7 +244,7 @@ export function ServerConfigScreen({
|
|
|
243
244
|
<View className="px-6 py-6">
|
|
244
245
|
<View className="mb-8 flex-row items-center justify-between">
|
|
245
246
|
<Pressable
|
|
246
|
-
onPress={() => router.back()}
|
|
247
|
+
onPress={withClickTracking("auth.server_config.back", () => router.back())}
|
|
247
248
|
accessibilityRole="button"
|
|
248
249
|
accessibilityLabel="Go back"
|
|
249
250
|
className="active:scale-98 h-10 w-10 items-center justify-center rounded-lg bg-secondary"
|
|
@@ -305,7 +306,7 @@ export function ServerConfigScreen({
|
|
|
305
306
|
</View>
|
|
306
307
|
|
|
307
308
|
<Pressable
|
|
308
|
-
onPress={testConnection}
|
|
309
|
+
onPress={withClickTracking("auth.server_config.test_connection", testConnection)}
|
|
309
310
|
disabled={isTestingConnection}
|
|
310
311
|
accessibilityRole="button"
|
|
311
312
|
accessibilityLabel="Test connection"
|
|
@@ -330,7 +331,7 @@ export function ServerConfigScreen({
|
|
|
330
331
|
</Pressable>
|
|
331
332
|
|
|
332
333
|
<Pressable
|
|
333
|
-
onPress={handleUseDefault}
|
|
334
|
+
onPress={withClickTracking("auth.server_config.use_default", handleUseDefault)}
|
|
334
335
|
accessibilityRole="button"
|
|
335
336
|
accessibilityLabel="Use default server"
|
|
336
337
|
className="active:scale-98 mb-3 w-full rounded-lg border border-border bg-background px-6 py-4"
|
|
@@ -344,7 +345,7 @@ export function ServerConfigScreen({
|
|
|
344
345
|
</Pressable>
|
|
345
346
|
|
|
346
347
|
<Pressable
|
|
347
|
-
onPress={handleClearConfiguration}
|
|
348
|
+
onPress={withClickTracking("auth.server_config.clear_configuration", handleClearConfiguration)}
|
|
348
349
|
accessibilityRole="button"
|
|
349
350
|
accessibilityLabel="Clear all settings"
|
|
350
351
|
className="active:scale-98 mb-3 w-full rounded-lg border border-destructive bg-background px-6 py-3"
|
|
@@ -358,7 +359,7 @@ export function ServerConfigScreen({
|
|
|
358
359
|
</Pressable>
|
|
359
360
|
|
|
360
361
|
<Pressable
|
|
361
|
-
onPress={handleSave}
|
|
362
|
+
onPress={withClickTracking("auth.server_config.save", handleSave)}
|
|
362
363
|
accessibilityRole="button"
|
|
363
364
|
accessibilityLabel="Save configuration"
|
|
364
365
|
className="active:scale-98 mb-6 w-full rounded-lg bg-primary px-6 py-4"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withClickTracking } from "../../services/telemetry";
|
|
1
2
|
import React from "react";
|
|
2
3
|
import { Linking, ScrollView, Text, View } from "react-native";
|
|
3
4
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
@@ -73,7 +74,8 @@ export function TermsOfServiceScreen({
|
|
|
73
74
|
<View className="flex-1" style={{ backgroundColor: isDark ? "#000000" : "#FFFFFF" }}>
|
|
74
75
|
<View className="px-6 pt-4 pb-6">
|
|
75
76
|
<View className="flex-row items-center justify-between mb-6">
|
|
76
|
-
<IconButton variant="ghost" size="sm"
|
|
77
|
+
<IconButton variant="ghost" size="sm" telemetryId="auth.terms_of_service.back"
|
|
78
|
+
onPress={() => router.back()}>
|
|
77
79
|
<Ionicons name="arrow-back" size={20} color={isDark ? "#FFFFFF" : "#000000"} />
|
|
78
80
|
</IconButton>
|
|
79
81
|
|
|
@@ -166,7 +168,7 @@ export function TermsOfServiceScreen({
|
|
|
166
168
|
<Text
|
|
167
169
|
className="font-mono text-sm underline"
|
|
168
170
|
style={{ color: isDark ? "#6BA3E5" : "#4A90E2" }}
|
|
169
|
-
onPress={() => Linking.openURL(`mailto:${supportEmail}`)}
|
|
171
|
+
onPress={withClickTracking("auth.terms_of_service.open_u_r_l", () => Linking.openURL(`mailto:${supportEmail}`))}
|
|
170
172
|
>
|
|
171
173
|
{supportEmail}
|
|
172
174
|
</Text>
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
CardHeader,
|
|
19
19
|
CardTitle,
|
|
20
20
|
} from "../../components/card";
|
|
21
|
+
import { withClickTracking } from "../../services/telemetry";
|
|
21
22
|
import { Input } from "../../components/input";
|
|
22
23
|
import AuthService from "../../services/auth.service";
|
|
23
24
|
import { useAuth } from "../../hooks/useAuth";
|
|
@@ -223,7 +224,8 @@ export function VerifyEmailScreen() {
|
|
|
223
224
|
<View className="mt-4 self-start absolute top-4 left-6">
|
|
224
225
|
<IconButton
|
|
225
226
|
variant="ghost"
|
|
226
|
-
|
|
227
|
+
telemetryId="auth.verify_email.logout"
|
|
228
|
+
onPress={() => logout()}
|
|
227
229
|
accessibilityLabel="Sign out"
|
|
228
230
|
accessibilityRole="button"
|
|
229
231
|
>
|
|
@@ -268,7 +270,7 @@ export function VerifyEmailScreen() {
|
|
|
268
270
|
) : (
|
|
269
271
|
<Text
|
|
270
272
|
className="text-base text-foreground underline"
|
|
271
|
-
onPress={handleResend}
|
|
273
|
+
onPress={withClickTracking("auth.verify_email.resend", handleResend)}
|
|
272
274
|
accessibilityRole="button"
|
|
273
275
|
accessibilityLabel="Resend verification code"
|
|
274
276
|
>
|
|
@@ -281,7 +283,8 @@ export function VerifyEmailScreen() {
|
|
|
281
283
|
<CardFooter className="flex-col items-center gap-1 border-t-0 pt-0">
|
|
282
284
|
<Button
|
|
283
285
|
className="h-14 w-full"
|
|
284
|
-
|
|
286
|
+
telemetryId="auth.verify_email.verify"
|
|
287
|
+
onPress={handleVerify}
|
|
285
288
|
isLoading={isLoading}
|
|
286
289
|
accessibilityLabel="Continue"
|
|
287
290
|
accessibilityRole="button"
|
|
@@ -294,7 +297,8 @@ export function VerifyEmailScreen() {
|
|
|
294
297
|
<Button
|
|
295
298
|
variant="ghost"
|
|
296
299
|
className="w-full"
|
|
297
|
-
|
|
300
|
+
telemetryId="auth.verify_email.logout_2"
|
|
301
|
+
onPress={() => logout()}
|
|
298
302
|
accessibilityLabel="Sign out"
|
|
299
303
|
accessibilityRole="button"
|
|
300
304
|
>
|
|
@@ -50,10 +50,12 @@ export function WelcomeScreen({ decoration, topAccessory }: WelcomeScreenProps)
|
|
|
50
50
|
) : null}
|
|
51
51
|
|
|
52
52
|
<View className="w-full max-w-[360px] gap-3">
|
|
53
|
-
<Button className="w-full"
|
|
53
|
+
<Button className="w-full" telemetryId="auth.welcome.push"
|
|
54
|
+
onPress={() => router.push(routes.login)}>
|
|
54
55
|
Sign In
|
|
55
56
|
</Button>
|
|
56
|
-
<Button variant="outline" className="w-full"
|
|
57
|
+
<Button variant="outline" className="w-full" telemetryId="auth.welcome.push_2"
|
|
58
|
+
onPress={() => router.push(routes.register)}>
|
|
57
59
|
Create Account
|
|
58
60
|
</Button>
|
|
59
61
|
</View>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__resetTelemetryForTests,
|
|
3
|
+
registerTelemetry,
|
|
4
|
+
trackClick,
|
|
5
|
+
withClickTracking,
|
|
6
|
+
} from "../telemetry";
|
|
7
|
+
|
|
8
|
+
describe("telemetry registry", () => {
|
|
9
|
+
afterEach(() => __resetTelemetryForTests());
|
|
10
|
+
|
|
11
|
+
it("is a no-op until a sink is registered", () => {
|
|
12
|
+
// The whole point of the default: a consumer that never opts in must not
|
|
13
|
+
// have this throw, buffer, or reach the network.
|
|
14
|
+
expect(() => trackClick("login.submit")).not.toThrow();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("forwards element and route to the registered sink", () => {
|
|
18
|
+
const sink = { trackClick: jest.fn() };
|
|
19
|
+
registerTelemetry(sink);
|
|
20
|
+
|
|
21
|
+
trackClick("login.submit", "/login");
|
|
22
|
+
|
|
23
|
+
expect(sink.trackClick).toHaveBeenCalledWith("login.submit", "/login");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("swallows a sink that throws", () => {
|
|
27
|
+
// A failing sink must never take down the press handler it is observing.
|
|
28
|
+
registerTelemetry({
|
|
29
|
+
trackClick: () => {
|
|
30
|
+
throw new Error("collector unreachable");
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(() => trackClick("login.submit")).not.toThrow();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("stops reporting once the sink is removed", () => {
|
|
38
|
+
const sink = { trackClick: jest.fn() };
|
|
39
|
+
registerTelemetry(sink);
|
|
40
|
+
registerTelemetry(null);
|
|
41
|
+
|
|
42
|
+
trackClick("login.submit");
|
|
43
|
+
|
|
44
|
+
expect(sink.trackClick).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("withClickTracking", () => {
|
|
48
|
+
it("returns the original handler when there is no id", () => {
|
|
49
|
+
// Identity, not a wrapper: an untagged control should not pay for a
|
|
50
|
+
// closure on every render.
|
|
51
|
+
const handler = jest.fn();
|
|
52
|
+
expect(withClickTracking(undefined, handler)).toBe(handler);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("reports before invoking the handler", () => {
|
|
56
|
+
const order: string[] = [];
|
|
57
|
+
registerTelemetry({ trackClick: () => order.push("tracked") });
|
|
58
|
+
const wrapped = withClickTracking("login.submit", () => order.push("handled"));
|
|
59
|
+
|
|
60
|
+
wrapped?.();
|
|
61
|
+
|
|
62
|
+
expect(order).toEqual(["tracked", "handled"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("still reports when there is no handler to call", () => {
|
|
66
|
+
const sink = { trackClick: jest.fn() };
|
|
67
|
+
registerTelemetry(sink);
|
|
68
|
+
|
|
69
|
+
withClickTracking("login.submit", undefined)?.();
|
|
70
|
+
|
|
71
|
+
expect(sink.trackClick).toHaveBeenCalledWith("login.submit", undefined);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("passes arguments and the return value through", () => {
|
|
75
|
+
registerTelemetry({ trackClick: jest.fn() });
|
|
76
|
+
const wrapped = withClickTracking("row.select", (id: never) => `picked ${String(id)}`);
|
|
77
|
+
|
|
78
|
+
expect(wrapped?.("abc" as never)).toBe("picked abc");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -98,6 +98,15 @@ export interface User {
|
|
|
98
98
|
mfa_enabled?: boolean;
|
|
99
99
|
mfa_verified?: boolean;
|
|
100
100
|
created_at?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Tenant the account belongs to. Only the multi-tenant IdP (access) sends
|
|
103
|
+
* these two; the single-tenant products have no tenant and carry a single
|
|
104
|
+
* `role` rather than a role set.
|
|
105
|
+
*/
|
|
106
|
+
tenant_id?: string;
|
|
107
|
+
tenant_slug?: string;
|
|
108
|
+
/** Effective role names, as resolved by the IdP (direct, assigned, or via a group). */
|
|
109
|
+
roles?: string[];
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
export interface AuthResponse {
|
package/src/services/index.ts
CHANGED
|
@@ -50,3 +50,12 @@ export {
|
|
|
50
50
|
resolveIcon,
|
|
51
51
|
type ResolvedIcon,
|
|
52
52
|
} from "./favicon";
|
|
53
|
+
|
|
54
|
+
// Telemetry registry. No-op until a host app calls registerTelemetry.
|
|
55
|
+
export {
|
|
56
|
+
registerTelemetry,
|
|
57
|
+
trackClick,
|
|
58
|
+
withClickTracking,
|
|
59
|
+
__resetTelemetryForTests,
|
|
60
|
+
type TelemetrySink,
|
|
61
|
+
} from "./telemetry";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Telemetry registry.
|
|
2
|
+
//
|
|
3
|
+
// This package deliberately knows nothing about how telemetry is collected,
|
|
4
|
+
// batched, authenticated or shipped. It only knows that an interaction
|
|
5
|
+
// happened and what it was called. The consuming app registers a sink at
|
|
6
|
+
// startup — the same arrangement as `configureApi` for HTTP — so one app can
|
|
7
|
+
// post to its own server, another to a collector, and a third to nothing at
|
|
8
|
+
// all.
|
|
9
|
+
//
|
|
10
|
+
// Nothing is registered by default, and every entry point is a no-op until
|
|
11
|
+
// something is. An app that never calls `registerTelemetry` behaves exactly as
|
|
12
|
+
// it did before this existed, which is what makes it safe to ship in a package
|
|
13
|
+
// that self-hosted deployments consume.
|
|
14
|
+
|
|
15
|
+
/** What a host app must provide to receive interaction events. */
|
|
16
|
+
export interface TelemetrySink {
|
|
17
|
+
/**
|
|
18
|
+
* An interactive element was activated.
|
|
19
|
+
*
|
|
20
|
+
* `element` is the author-assigned, stable id — not label text, which
|
|
21
|
+
* changes with copy and can carry user data.
|
|
22
|
+
*/
|
|
23
|
+
trackClick(element: string, route?: string): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class TelemetryStore {
|
|
27
|
+
private sink: TelemetrySink | null = null;
|
|
28
|
+
|
|
29
|
+
register(sink: TelemetrySink | null): void {
|
|
30
|
+
this.sink = sink;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
trackClick(element: string, route?: string): void {
|
|
34
|
+
// Never allowed to throw into a press handler. A telemetry sink that
|
|
35
|
+
// fails must not stop the button from doing its job.
|
|
36
|
+
try {
|
|
37
|
+
this.sink?.trackClick(element, route);
|
|
38
|
+
} catch {
|
|
39
|
+
// Intentionally swallowed.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
reset(): void {
|
|
44
|
+
this.sink = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const store = new TelemetryStore();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Install a telemetry sink. Call once at app startup; pass `null` to remove.
|
|
52
|
+
*/
|
|
53
|
+
export function registerTelemetry(sink: TelemetrySink | null): void {
|
|
54
|
+
store.register(sink);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Report an element activation. Safe to call unconditionally — it is a no-op
|
|
59
|
+
* when no sink is registered.
|
|
60
|
+
*/
|
|
61
|
+
export function trackClick(element: string, route?: string): void {
|
|
62
|
+
store.trackClick(element, route);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Wrap a press handler so activating the element is reported first.
|
|
67
|
+
*
|
|
68
|
+
* Used by the primitives to implement `telemetryId`, and exported so app code
|
|
69
|
+
* can tag anything the primitives do not cover.
|
|
70
|
+
*/
|
|
71
|
+
export function withClickTracking<F extends (...args: never[]) => unknown>(
|
|
72
|
+
element: string | undefined,
|
|
73
|
+
handler: F,
|
|
74
|
+
): F;
|
|
75
|
+
export function withClickTracking<F extends (...args: never[]) => unknown>(
|
|
76
|
+
element: string | undefined,
|
|
77
|
+
handler: F | null | undefined,
|
|
78
|
+
): F | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Wrap a press handler so activating the element is reported first.
|
|
81
|
+
*
|
|
82
|
+
* Used by the primitives to implement `telemetryId`, and exported so app code
|
|
83
|
+
* can tag anything the primitives do not cover.
|
|
84
|
+
*
|
|
85
|
+
* Overloaded so the wrapper is exactly as optional as the handler it wraps: a
|
|
86
|
+
* required `onPress` stays required, and React Native's nullable `onPress` is
|
|
87
|
+
* accepted without every call site having to coalesce it.
|
|
88
|
+
*/
|
|
89
|
+
export function withClickTracking(
|
|
90
|
+
element: string | undefined,
|
|
91
|
+
handler: ((...args: never[]) => unknown) | null | undefined,
|
|
92
|
+
): ((...args: never[]) => unknown) | undefined {
|
|
93
|
+
// Untagged controls keep their original handler rather than gaining a
|
|
94
|
+
// closure per render for nothing.
|
|
95
|
+
if (element === undefined) {
|
|
96
|
+
return handler ?? undefined;
|
|
97
|
+
}
|
|
98
|
+
return (...args: never[]) => {
|
|
99
|
+
trackClick(element);
|
|
100
|
+
return handler?.(...args);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Test seam: drop the registered sink so cases cannot leak into each other. */
|
|
105
|
+
export function __resetTelemetryForTests(): void {
|
|
106
|
+
store.reset();
|
|
107
|
+
}
|