@atzentis/auth-expo 0.1.0 → 0.3.0

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/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- import { StyleSheet, View, Text, TextInput, KeyboardAvoidingView, ScrollView, Platform, ActivityIndicator, Alert, FlatList, RefreshControl, ActionSheetIOS, Pressable, Image } from 'react-native';
1
+ import { StyleSheet, View, Text, TextInput, Pressable, KeyboardAvoidingView, ScrollView, Platform, ActivityIndicator, Alert, FlatList, RefreshControl, ActionSheetIOS, Image } from 'react-native';
2
2
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
- import { authKeys, forgotPasswordSchema, credentialLoginSchema, magicLinkSchema, phoneLoginSchema, otpVerifySchema, resetPasswordSchema, signupSchema, totpSchema, backupCodeSchema, changeEmailSchema, changePasswordSchema, updateNameSchema, updateUsernameSchema } from '@atzentis/auth-sdk';
3
+ import { InvalidConfigError, authKeys, RequestFailedError, forgotPasswordSchema, credentialLoginSchema, magicLinkSchema, NotImplementedError, phoneLoginSchema, otpVerifySchema, resetPasswordSchema, signupSchema, totpSchema, backupCodeSchema, changeEmailSchema, changePasswordSchema, updateNameSchema, updateUsernameSchema } from '@atzentis/auth-sdk';
4
4
  import { QueryClient, QueryClientProvider, useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
5
- import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react';
5
+ import React, { forwardRef, useState, useMemo, use, useCallback, useEffect } from 'react';
6
6
  import { en } from '@atzentis/auth-locales';
7
7
  import { expoClient } from '@better-auth/expo/client';
8
8
  import { createAuthClient } from 'better-auth/react';
9
9
  import { getLocales } from 'expo-localization';
10
10
  import * as SecureStore from 'expo-secure-store';
11
- import { EyeOff, Eye, AlertCircle, CheckCircle2, Fingerprint } from 'lucide-react-native';
11
+ import { EyeOff, Eye, AlertCircle, CheckCircle2 } from 'lucide-react-native';
12
12
  import { Host, Button } from '@expo/ui/swift-ui';
13
13
  import { useRouter } from 'expo-router';
14
14
  import * as Haptics from 'expo-haptics';
@@ -59,7 +59,9 @@ function AuthProvider({
59
59
  children,
60
60
  locales,
61
61
  localization: localizationOverride,
62
- queryClient: externalQueryClient
62
+ queryClient: externalQueryClient,
63
+ scheme,
64
+ storagePrefix
63
65
  }) {
64
66
  const queryClient = useMemo(
65
67
  () => externalQueryClient ?? new QueryClient(),
@@ -70,13 +72,13 @@ function AuthProvider({
70
72
  baseURL,
71
73
  plugins: [
72
74
  expoClient({
73
- scheme: "atzentis",
74
- storagePrefix: "atzentis",
75
+ scheme,
76
+ storagePrefix: storagePrefix ?? scheme,
75
77
  storage: SecureStore
76
78
  })
77
79
  ]
78
80
  }),
79
- [baseURL]
81
+ [baseURL, scheme, storagePrefix]
80
82
  );
81
83
  const resolvedLocalization = useMemo(() => {
82
84
  const deviceLocales = getLocales();
@@ -92,9 +94,11 @@ function AuthProvider({
92
94
 
93
95
  // src/hooks/use-auth-client.ts
94
96
  function useAuthClient() {
95
- const client = React.use(AuthClientContext);
97
+ const client = use(AuthClientContext);
96
98
  if (!client) {
97
- throw new Error("useAuthClient must be used within an AuthProvider");
99
+ throw new InvalidConfigError({
100
+ message: "useAuthClient must be used within an AuthProvider"
101
+ });
98
102
  }
99
103
  return client;
100
104
  }
@@ -118,7 +122,7 @@ function AuthGate({ children, fallback = null }) {
118
122
  }
119
123
  function ErrorFeedback({ message }) {
120
124
  return /* @__PURE__ */ jsxs(View, { style: styles2.container, accessibilityRole: "alert", accessibilityLiveRegion: "polite", children: [
121
- /* @__PURE__ */ jsx(AlertCircle, { size: 16, color: "#ef4444" }),
125
+ /* @__PURE__ */ jsx(AlertCircle, { size: 16 }),
122
126
  /* @__PURE__ */ jsx(Text, { style: styles2.text, children: message })
123
127
  ] });
124
128
  }
@@ -133,7 +137,7 @@ var styles2 = StyleSheet.create({
133
137
  },
134
138
  text: { fontSize: 14, color: "#ef4444", flex: 1 }
135
139
  });
136
- var FormInput = React.forwardRef(
140
+ var FormInput = forwardRef(
137
141
  ({ error, label, style, ...props }, ref) => {
138
142
  return /* @__PURE__ */ jsxs(View, { style: { gap: 4 }, children: [
139
143
  label && /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, fontWeight: "500", color: "#1f2937" }, children: label }),
@@ -164,7 +168,7 @@ var FormInput = React.forwardRef(
164
168
  }
165
169
  );
166
170
  FormInput.displayName = "FormInput";
167
- var FormPasswordInput = React.forwardRef(
171
+ var FormPasswordInput = forwardRef(
168
172
  ({ error, label, style, ...props }, ref) => {
169
173
  const [visible, setVisible] = useState(false);
170
174
  return /* @__PURE__ */ jsxs(View, { style: { gap: 4 }, children: [
@@ -195,8 +199,11 @@ var FormPasswordInput = React.forwardRef(
195
199
  }
196
200
  ),
197
201
  /* @__PURE__ */ jsx(
198
- View,
202
+ Pressable,
199
203
  {
204
+ onPress: () => setVisible((v) => !v),
205
+ accessibilityRole: "button",
206
+ accessibilityLabel: visible ? "Hide password" : "Show password",
200
207
  style: {
201
208
  position: "absolute",
202
209
  right: 8,
@@ -206,9 +213,7 @@ var FormPasswordInput = React.forwardRef(
206
213
  alignItems: "center",
207
214
  width: 32
208
215
  },
209
- accessibilityRole: "button",
210
- onPress: () => setVisible((v) => !v),
211
- children: visible ? /* @__PURE__ */ jsx(EyeOff, { size: 20, color: "#6b7280" }) : /* @__PURE__ */ jsx(Eye, { size: 20, color: "#6b7280" })
216
+ children: visible ? /* @__PURE__ */ jsx(EyeOff, { size: 20 }) : /* @__PURE__ */ jsx(Eye, { size: 20 })
212
217
  }
213
218
  )
214
219
  ] }),
@@ -218,9 +223,9 @@ var FormPasswordInput = React.forwardRef(
218
223
  );
219
224
  FormPasswordInput.displayName = "FormPasswordInput";
220
225
  function useAuthLocalization() {
221
- return React.use(LocalizationContext);
226
+ return use(LocalizationContext);
222
227
  }
223
- function OAuthButton({ disabled = false, onError, onSuccess, provider }) {
228
+ function OAuthButton({ onError, onSuccess, provider }) {
224
229
  const { localization: L } = useAuthLocalization();
225
230
  const router = useRouter();
226
231
  const client = useAuthClient();
@@ -229,16 +234,16 @@ function OAuthButton({ disabled = false, onError, onSuccess, provider }) {
229
234
  mutationFn: async () => {
230
235
  const response = await client.signIn.social({ provider });
231
236
  if (response.error) {
232
- throw new Error(response.error.message ?? "OAuth sign-in failed");
237
+ throw new RequestFailedError({ message: response.error.message ?? "OAuth sign-in failed" });
233
238
  }
234
239
  return response.data;
235
240
  },
236
241
  onSuccess: (data) => {
237
- if (data?.user) {
238
- queryClient.setQueryData(authKeys.user(), data.user);
242
+ if ("url" in data && data.url) {
243
+ queryClient.invalidateQueries({ queryKey: authKeys.session() });
239
244
  }
240
- if (data?.session) {
241
- queryClient.setQueryData(authKeys.session(), data.session);
245
+ if ("user" in data && data.user) {
246
+ queryClient.setQueryData(authKeys.user(), data.user);
242
247
  }
243
248
  }
244
249
  });
@@ -260,15 +265,7 @@ function OAuthButton({ disabled = false, onError, onSuccess, provider }) {
260
265
  }, [oauthMutation, router, onSuccess, onError]);
261
266
  const label = `${L.CONTINUE_WITH} ${provider.charAt(0).toUpperCase()}${provider.slice(1)}`;
262
267
  const loadingLabel = L.SIGNING_IN;
263
- return /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
264
- Button,
265
- {
266
- onPress: handlePress,
267
- disabled: disabled || oauthMutation.isPending,
268
- variant: "bordered",
269
- children: oauthMutation.isPending ? loadingLabel : label
270
- }
271
- ) });
268
+ return /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handlePress, label: oauthMutation.isPending ? loadingLabel : label }) });
272
269
  }
273
270
  function ProtectedScreen({ children, fallback = null }) {
274
271
  const { data: session, isLoading } = useSession();
@@ -284,7 +281,7 @@ function ProtectedScreen({ children, fallback = null }) {
284
281
  }
285
282
  function SuccessFeedback({ message }) {
286
283
  return /* @__PURE__ */ jsxs(View, { style: styles3.container, accessibilityRole: "alert", accessibilityLiveRegion: "polite", children: [
287
- /* @__PURE__ */ jsx(CheckCircle2, { size: 16, color: "#22c55e" }),
284
+ /* @__PURE__ */ jsx(CheckCircle2, { size: 16 }),
288
285
  /* @__PURE__ */ jsx(Text, { style: styles3.text, children: message })
289
286
  ] });
290
287
  }
@@ -300,19 +297,21 @@ var styles3 = StyleSheet.create({
300
297
  text: { fontSize: 14, color: "#22c55e", flex: 1 }
301
298
  });
302
299
  function SubmitButton({
303
- disabled,
304
300
  label,
305
301
  loading,
306
302
  loadingLabel,
307
- onPress
303
+ onPress,
304
+ disabled = false
308
305
  }) {
306
+ const isDisabled = disabled || loading;
309
307
  const handlePress = () => {
308
+ if (isDisabled) return;
310
309
  if (process.env.EXPO_OS === "ios") {
311
310
  Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
312
311
  }
313
312
  onPress();
314
313
  };
315
- return /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { disabled: loading || disabled, onPress: handlePress, variant: "borderedProminent", children: loading ? loadingLabel : label }) });
314
+ return /* @__PURE__ */ jsx(Host, { matchContents: true, style: { opacity: isDisabled ? 0.5 : 1 }, children: /* @__PURE__ */ jsx(Button, { onPress: handlePress, label: loading ? loadingLabel : label }) });
316
315
  }
317
316
  function EmailVerificationScreen({ onSuccess, token }) {
318
317
  const { localization: L } = useAuthLocalization();
@@ -321,9 +320,9 @@ function EmailVerificationScreen({ onSuccess, token }) {
321
320
  const [status, setStatus] = useState(token ? "loading" : "no_token");
322
321
  const verifyMutation = useMutation({
323
322
  mutationFn: async (verifyToken) => {
324
- const response = await client.verifyEmail?.({ token: verifyToken });
323
+ const response = await client.verifyEmail?.({ query: { token: verifyToken } });
325
324
  if (response?.error) {
326
- throw new Error(response.error.message ?? "Verification failed");
325
+ throw new RequestFailedError({ message: response.error.message ?? L.REQUEST_FAILED });
327
326
  }
328
327
  return response?.data;
329
328
  }
@@ -378,12 +377,12 @@ function EmailVerificationScreen({ onSuccess, token }) {
378
377
  ] }),
379
378
  status === "error" && /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
380
379
  /* @__PURE__ */ jsx(ErrorFeedback, { message: verifyMutation.error?.message ?? L.REQUEST_FAILED }),
381
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleRetry, variant: "bordered", children: L.VERIFY }) }),
382
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
380
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleRetry, label: L.VERIFY }) }),
381
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
383
382
  ] }),
384
383
  status === "no_token" && /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
385
384
  /* @__PURE__ */ jsx(ErrorFeedback, { message: L.REQUEST_FAILED }),
386
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
385
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
387
386
  ] })
388
387
  ] }) });
389
388
  }
@@ -391,11 +390,16 @@ function useRequestPasswordReset() {
391
390
  const client = useAuthClient();
392
391
  return useMutation({
393
392
  mutationFn: async (data) => {
394
- const response = await client.forgetPassword(data);
395
- if (response.error) {
396
- throw new Error(response.error.message ?? "Password reset request failed");
393
+ try {
394
+ await client.requestPasswordReset({
395
+ email: data.email,
396
+ redirectTo: data.redirectTo
397
+ });
398
+ return true;
399
+ } catch (error) {
400
+ const message = error instanceof Error && error.message ? error.message : "Password reset request failed";
401
+ throw new RequestFailedError({ message });
397
402
  }
398
- return true;
399
403
  }
400
404
  });
401
405
  }
@@ -451,7 +455,7 @@ function ForgotPasswordScreen({ onSuccess }) {
451
455
  render: ({ field: { onChange, onBlur, value }, fieldState: { error } }) => /* @__PURE__ */ jsx(
452
456
  FormInput,
453
457
  {
454
- label: L.EMAIL_PLACEHOLDER,
458
+ label: L.EMAIL,
455
459
  placeholder: L.EMAIL_PLACEHOLDER,
456
460
  autoCapitalize: "none",
457
461
  keyboardType: "email-address",
@@ -471,15 +475,20 @@ function ForgotPasswordScreen({ onSuccess }) {
471
475
  label: L.FORGOT_PASSWORD_ACTION,
472
476
  loading: forgotMutation.isPending,
473
477
  loadingLabel: L.SENDING,
474
- onPress: form.handleSubmit(handleSubmit),
475
- disabled: !form.formState.isValid
478
+ onPress: form.handleSubmit(handleSubmit)
476
479
  }
477
480
  ),
478
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
481
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
479
482
  ] }) : /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
480
483
  /* @__PURE__ */ jsx(SuccessFeedback, { message: `${L.FORGOT_PASSWORD_EMAIL} ${form.getValues("email")}` }),
481
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleResend, disabled: cooldown > 0, variant: "borderless", children: cooldown > 0 ? `${L.RESEND} (${cooldown}s)` : L.RESEND }) }),
482
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
484
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
485
+ Button,
486
+ {
487
+ onPress: handleResend,
488
+ label: cooldown > 0 ? `${L.RESEND} (${cooldown}s)` : L.RESEND
489
+ }
490
+ ) }),
491
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
483
492
  ] })
484
493
  ] }) });
485
494
  }
@@ -490,16 +499,14 @@ function useLogin() {
490
499
  mutationFn: async (data) => {
491
500
  const response = await client.signIn.email(data);
492
501
  if (response.error) {
493
- throw new Error(response.error.message ?? "Login failed");
502
+ throw new RequestFailedError({ message: response.error.message ?? "Login failed" });
494
503
  }
495
504
  return response.data;
496
505
  },
497
506
  onSuccess: (data) => {
498
507
  if (data?.user) {
499
508
  queryClient.setQueryData(authKeys.user(), data.user);
500
- }
501
- if (data?.session) {
502
- queryClient.setQueryData(authKeys.session(), data.session);
509
+ queryClient.invalidateQueries({ queryKey: authKeys.session() });
503
510
  }
504
511
  }
505
512
  });
@@ -548,7 +555,7 @@ function LoginScreen({
548
555
  render: ({ field: { onChange, onBlur, value }, fieldState: { error } }) => /* @__PURE__ */ jsx(
549
556
  FormInput,
550
557
  {
551
- label: L.SIGN_IN_USERNAME_PLACEHOLDER,
558
+ label: L.EMAIL_OR_USERNAME,
552
559
  placeholder: L.EMAIL_PLACEHOLDER,
553
560
  autoCapitalize: "none",
554
561
  keyboardType: "email-address",
@@ -588,24 +595,26 @@ function LoginScreen({
588
595
  label: L.SIGN_IN_ACTION,
589
596
  loading: loginMutation.isPending,
590
597
  loadingLabel: L.SIGNING_IN,
591
- onPress: form.handleSubmit(onSubmit),
592
- disabled: !form.formState.isValid
598
+ onPress: form.handleSubmit(onSubmit)
593
599
  }
594
600
  ),
595
- biometricAvailable && process.env.EXPO_OS === "ios" && onBiometricPress && /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsxs(Button, { onPress: onBiometricPress, variant: "bordered", children: [
596
- /* @__PURE__ */ jsx(Fingerprint, { size: 20, color: "#6b7280" }),
597
- L.SIGN_IN
598
- ] }) }),
601
+ biometricAvailable && process.env.EXPO_OS === "ios" && onBiometricPress && /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: onBiometricPress }) }),
599
602
  /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", justifyContent: "space-between", marginTop: 8 }, children: [
600
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.push("/signup"), variant: "borderless", children: L.DONT_HAVE_AN_ACCOUNT }) }),
601
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.push("/forgot-password"), variant: "borderless", children: L.FORGOT_PASSWORD_LINK }) })
603
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.push("/signup"), label: L.DONT_HAVE_AN_ACCOUNT }) }),
604
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
605
+ Button,
606
+ {
607
+ onPress: () => router.push("/forgot-password"),
608
+ label: L.FORGOT_PASSWORD_LINK
609
+ }
610
+ ) })
602
611
  ] })
603
612
  ] }) });
604
613
  }
605
614
  function MagicLinkScreen({ onSuccess, token }) {
606
615
  const { localization: L } = useAuthLocalization();
607
616
  const router = useRouter();
608
- const client = useAuthClient();
617
+ useAuthClient();
609
618
  const [state, setState] = useState(
610
619
  token ? { step: "verifying", token } : { step: "email" }
611
620
  );
@@ -616,21 +625,13 @@ function MagicLinkScreen({ onSuccess, token }) {
616
625
  mode: "onChange"
617
626
  });
618
627
  const sendMutation = useMutation({
619
- mutationFn: async (data) => {
620
- const response = await client.signIn.magicLink?.({ email: data.email });
621
- if (response?.error) {
622
- throw new Error(response.error.message ?? "Failed to send magic link");
623
- }
624
- return response?.data;
628
+ mutationFn: async (_data) => {
629
+ throw new NotImplementedError({ feature: "Magic link authentication" });
625
630
  }
626
631
  });
627
632
  const verifyMutation = useMutation({
628
- mutationFn: async (verifyToken) => {
629
- const response = await client.magicLink?.verify?.({ token: verifyToken });
630
- if (response?.error) {
631
- throw new Error(response.error.message ?? "Invalid or expired link");
632
- }
633
- return response?.data;
633
+ mutationFn: async (_verifyToken) => {
634
+ throw new NotImplementedError({ feature: "Magic link verification" });
634
635
  }
635
636
  });
636
637
  const handleSend = useCallback(
@@ -682,7 +683,7 @@ function MagicLinkScreen({ onSuccess, token }) {
682
683
  render: ({ field: { onChange, onBlur, value }, fieldState: { error } }) => /* @__PURE__ */ jsx(
683
684
  FormInput,
684
685
  {
685
- label: L.EMAIL_PLACEHOLDER,
686
+ label: L.EMAIL,
686
687
  placeholder: L.EMAIL_PLACEHOLDER,
687
688
  autoCapitalize: "none",
688
689
  keyboardType: "email-address",
@@ -702,8 +703,7 @@ function MagicLinkScreen({ onSuccess, token }) {
702
703
  label: L.MAGIC_LINK_ACTION,
703
704
  loading: sendMutation.isPending,
704
705
  loadingLabel: L.SENDING,
705
- onPress: form.handleSubmit(handleSend),
706
- disabled: !form.formState.isValid
706
+ onPress: form.handleSubmit(handleSend)
707
707
  }
708
708
  )
709
709
  ] }),
@@ -721,9 +721,7 @@ function MagicLinkScreen({ onSuccess, token }) {
721
721
  );
722
722
  }
723
723
  },
724
- disabled: cooldown > 0,
725
- variant: "borderless",
726
- children: cooldown > 0 ? `${L.RESEND} (${cooldown}s)` : L.RESEND
724
+ label: cooldown > 0 ? `${L.RESEND} (${cooldown}s)` : L.RESEND
727
725
  }
728
726
  ) })
729
727
  ] }),
@@ -731,14 +729,14 @@ function MagicLinkScreen({ onSuccess, token }) {
731
729
  state.step === "success" && /* @__PURE__ */ jsx(SuccessFeedback, { message: L.REDIRECTING }),
732
730
  state.step === "error" && /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
733
731
  /* @__PURE__ */ jsx(ErrorFeedback, { message: state.message }),
734
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => setState({ step: "email" }), variant: "bordered", children: L.GO_BACK }) })
732
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => setState({ step: "email" }), label: L.GO_BACK }) })
735
733
  ] })
736
734
  ] }) });
737
735
  }
738
736
  function PhoneLoginScreen({ onSuccess }) {
739
737
  const { localization: L } = useAuthLocalization();
740
738
  const router = useRouter();
741
- const client = useAuthClient();
739
+ useAuthClient();
742
740
  const [state, setState] = useState({ step: 1 });
743
741
  const [cooldown, setCooldown] = useState(0);
744
742
  const phoneForm = useForm({
@@ -752,23 +750,13 @@ function PhoneLoginScreen({ onSuccess }) {
752
750
  mode: "onChange"
753
751
  });
754
752
  const sendOtpMutation = useMutation({
755
- mutationFn: async (data) => {
756
- const response = await client.phoneNumber?.sendVerificationCode({
757
- phoneNumber: `${data.countryCode}${data.phoneNumber}`
758
- });
759
- if (response?.error) {
760
- throw new Error(response.error.message ?? "Failed to send code");
761
- }
762
- return response?.data;
753
+ mutationFn: async (_data) => {
754
+ throw new NotImplementedError({ feature: "Phone number authentication" });
763
755
  }
764
756
  });
765
757
  const verifyOtpMutation = useMutation({
766
- mutationFn: async (data) => {
767
- const response = await client.phoneNumber?.verifyPhoneNumber(data);
768
- if (response?.error) {
769
- throw new Error(response.error.message ?? "Invalid code");
770
- }
771
- return response?.data;
758
+ mutationFn: async (_data) => {
759
+ throw new NotImplementedError({ feature: "Phone number verification" });
772
760
  }
773
761
  });
774
762
  const handleSendOtp = useCallback(
@@ -869,8 +857,7 @@ function PhoneLoginScreen({ onSuccess }) {
869
857
  label: L.SEND_VERIFICATION_CODE,
870
858
  loading: sendOtpMutation.isPending,
871
859
  loadingLabel: L.SENDING_VERIFICATION_CODE,
872
- onPress: phoneForm.handleSubmit(handleSendOtp),
873
- disabled: !phoneForm.formState.isValid
860
+ onPress: phoneForm.handleSubmit(handleSendOtp)
874
861
  }
875
862
  )
876
863
  ] }) : /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
@@ -905,13 +892,18 @@ function PhoneLoginScreen({ onSuccess }) {
905
892
  label: L.VERIFY,
906
893
  loading: verifyOtpMutation.isPending,
907
894
  loadingLabel: L.VERIFYING,
908
- onPress: otpForm.handleSubmit(handleVerifyOtp),
909
- disabled: !otpForm.formState.isValid
895
+ onPress: otpForm.handleSubmit(handleVerifyOtp)
910
896
  }
911
897
  ),
912
898
  /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", justifyContent: "space-between" }, children: [
913
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => setState({ step: 1 }), variant: "borderless", children: L.GO_BACK }) }),
914
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleResend, disabled: cooldown > 0, variant: "borderless", children: cooldown > 0 ? `${L.RESEND} (${cooldown}s)` : L.RESEND }) })
899
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => setState({ step: 1 }), label: L.GO_BACK }) }),
900
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
901
+ Button,
902
+ {
903
+ onPress: handleResend,
904
+ label: cooldown > 0 ? `${L.RESEND} (${cooldown}s)` : L.RESEND
905
+ }
906
+ ) })
915
907
  ] })
916
908
  ] })
917
909
  ] }) });
@@ -922,7 +914,9 @@ function useResetPassword() {
922
914
  mutationFn: async (data) => {
923
915
  const response = await client.resetPassword(data);
924
916
  if (response.error) {
925
- throw new Error(response.error.message ?? "Password reset failed");
917
+ throw new RequestFailedError({
918
+ message: response.error.message ?? "Password reset failed"
919
+ });
926
920
  }
927
921
  return true;
928
922
  }
@@ -959,13 +953,13 @@ function ResetPasswordScreen({ onSuccess, token }) {
959
953
  return /* @__PURE__ */ jsx(AuthContainer, { children: /* @__PURE__ */ jsxs(View, { style: { gap: 24 }, children: [
960
954
  /* @__PURE__ */ jsx(View, { style: { gap: 4, alignItems: "center" }, children: /* @__PURE__ */ jsx(Text, { style: { fontSize: 24, fontWeight: "700", color: "#1f2937" }, children: L.RESET_PASSWORD }) }),
961
955
  /* @__PURE__ */ jsx(ErrorFeedback, { message: L.REQUEST_FAILED }),
962
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
956
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
963
957
  ] }) });
964
958
  }
965
959
  if (resetMutation.isSuccess) {
966
960
  return /* @__PURE__ */ jsx(AuthContainer, { children: /* @__PURE__ */ jsxs(View, { style: { gap: 24 }, children: [
967
961
  /* @__PURE__ */ jsx(SuccessFeedback, { message: L.RESET_PASSWORD_SUCCESS }),
968
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.replace("/login"), variant: "bordered", children: L.SIGN_IN }) }) })
962
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.replace("/login"), label: L.SIGN_IN }) }) })
969
963
  ] }) });
970
964
  }
971
965
  return /* @__PURE__ */ jsx(AuthContainer, { children: /* @__PURE__ */ jsxs(View, { style: { gap: 24 }, children: [
@@ -1017,11 +1011,10 @@ function ResetPasswordScreen({ onSuccess, token }) {
1017
1011
  label: L.RESET_PASSWORD_ACTION,
1018
1012
  loading: resetMutation.isPending,
1019
1013
  loadingLabel: L.LOADING,
1020
- onPress: form.handleSubmit(handleSubmit),
1021
- disabled: !form.formState.isValid
1014
+ onPress: form.handleSubmit(handleSubmit)
1022
1015
  }
1023
1016
  ),
1024
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
1017
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
1025
1018
  ] }) });
1026
1019
  }
1027
1020
  function useSignup() {
@@ -1031,16 +1024,14 @@ function useSignup() {
1031
1024
  mutationFn: async (data) => {
1032
1025
  const response = await client.signUp.email(data);
1033
1026
  if (response.error) {
1034
- throw new Error(response.error.message ?? "Signup failed");
1027
+ throw new RequestFailedError({ message: response.error.message ?? "Signup failed" });
1035
1028
  }
1036
1029
  return response.data;
1037
1030
  },
1038
1031
  onSuccess: (data) => {
1039
1032
  if (data?.user) {
1040
1033
  queryClient.setQueryData(authKeys.user(), data.user);
1041
- }
1042
- if (data?.session) {
1043
- queryClient.setQueryData(authKeys.session(), data.session);
1034
+ queryClient.invalidateQueries({ queryKey: authKeys.session() });
1044
1035
  }
1045
1036
  }
1046
1037
  });
@@ -1087,7 +1078,7 @@ function SignupScreen({ onSuccess }) {
1087
1078
  render: ({ field: { onChange, onBlur, value }, fieldState: { error } }) => /* @__PURE__ */ jsx(
1088
1079
  FormInput,
1089
1080
  {
1090
- label: L.NAME_PLACEHOLDER,
1081
+ label: L.NAME,
1091
1082
  placeholder: L.NAME_PLACEHOLDER,
1092
1083
  autoCapitalize: "words",
1093
1084
  editable: !signupMutation.isPending,
@@ -1107,7 +1098,7 @@ function SignupScreen({ onSuccess }) {
1107
1098
  render: ({ field: { onChange, onBlur, value }, fieldState: { error } }) => /* @__PURE__ */ jsx(
1108
1099
  FormInput,
1109
1100
  {
1110
- label: L.EMAIL_PLACEHOLDER,
1101
+ label: L.EMAIL,
1111
1102
  placeholder: L.EMAIL_PLACEHOLDER,
1112
1103
  autoCapitalize: "none",
1113
1104
  keyboardType: "email-address",
@@ -1147,17 +1138,16 @@ function SignupScreen({ onSuccess }) {
1147
1138
  label: L.SIGN_UP_ACTION,
1148
1139
  loading: signupMutation.isPending,
1149
1140
  loadingLabel: L.SIGNING_UP,
1150
- onPress: form.handleSubmit(onSubmit),
1151
- disabled: !form.formState.isValid
1141
+ onPress: form.handleSubmit(onSubmit)
1152
1142
  }
1153
1143
  ),
1154
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.push("/login"), variant: "borderless", children: L.ALREADY_HAVE_AN_ACCOUNT }) }) })
1144
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.push("/login"), label: L.ALREADY_HAVE_AN_ACCOUNT }) }) })
1155
1145
  ] }) });
1156
1146
  }
1157
1147
  function TwoFactorScreen({ onSuccess }) {
1158
1148
  const { localization: L } = useAuthLocalization();
1159
1149
  const router = useRouter();
1160
- const client = useAuthClient();
1150
+ useAuthClient();
1161
1151
  const [mode, setMode] = useState("totp");
1162
1152
  const totpForm = useForm({
1163
1153
  resolver: zodResolver(totpSchema),
@@ -1170,12 +1160,8 @@ function TwoFactorScreen({ onSuccess }) {
1170
1160
  mode: "onChange"
1171
1161
  });
1172
1162
  const verifyMutation = useMutation({
1173
- mutationFn: async (data) => {
1174
- const response = await client.twoFactor?.verifyTotp?.(data);
1175
- if (response?.error) {
1176
- throw new Error(response.error.message ?? "Invalid code");
1177
- }
1178
- return response?.data;
1163
+ mutationFn: async (_data) => {
1164
+ throw new NotImplementedError({ feature: "Two-factor verification" });
1179
1165
  }
1180
1166
  });
1181
1167
  const handleTotpSubmit = useCallback(
@@ -1218,22 +1204,8 @@ function TwoFactorScreen({ onSuccess }) {
1218
1204
  /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, color: "#6b7280" }, children: L.TWO_FACTOR_DESCRIPTION })
1219
1205
  ] }),
1220
1206
  /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", gap: 8 }, children: [
1221
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
1222
- Button,
1223
- {
1224
- onPress: () => setMode("totp"),
1225
- variant: mode === "totp" ? "borderedProminent" : "bordered",
1226
- children: L.USE_AUTHENTICATOR
1227
- }
1228
- ) }),
1229
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
1230
- Button,
1231
- {
1232
- onPress: () => setMode("backup"),
1233
- variant: mode === "backup" ? "borderedProminent" : "bordered",
1234
- children: L.USE_BACKUP_CODE
1235
- }
1236
- ) })
1207
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => setMode("totp"), label: L.USE_AUTHENTICATOR }) }),
1208
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => setMode("backup"), label: L.USE_BACKUP_CODE }) })
1237
1209
  ] }),
1238
1210
  mode === "totp" ? /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
1239
1211
  /* @__PURE__ */ jsx(
@@ -1264,8 +1236,7 @@ function TwoFactorScreen({ onSuccess }) {
1264
1236
  label: L.TWO_FACTOR_ACTION,
1265
1237
  loading: verifyMutation.isPending,
1266
1238
  loadingLabel: L.VERIFYING,
1267
- onPress: totpForm.handleSubmit(handleTotpSubmit),
1268
- disabled: !totpForm.formState.isValid
1239
+ onPress: totpForm.handleSubmit(handleTotpSubmit)
1269
1240
  }
1270
1241
  )
1271
1242
  ] }) : /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
@@ -1296,8 +1267,7 @@ function TwoFactorScreen({ onSuccess }) {
1296
1267
  label: L.TWO_FACTOR_ACTION,
1297
1268
  loading: verifyMutation.isPending,
1298
1269
  loadingLabel: L.VERIFYING,
1299
- onPress: backupForm.handleSubmit(handleBackupSubmit),
1300
- disabled: !backupForm.formState.isValid
1270
+ onPress: backupForm.handleSubmit(handleBackupSubmit)
1301
1271
  }
1302
1272
  )
1303
1273
  ] })
@@ -1370,7 +1340,7 @@ function ChangeEmailScreen({ onSuccess, redirectUrl }) {
1370
1340
  callbackURL: redirectUrl
1371
1341
  });
1372
1342
  if (response.error) {
1373
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
1343
+ throw new RequestFailedError({ message: response.error.message ?? L.REQUEST_FAILED });
1374
1344
  }
1375
1345
  return response.data;
1376
1346
  }
@@ -1435,15 +1405,20 @@ function ChangeEmailScreen({ onSuccess, redirectUrl }) {
1435
1405
  label: L.CHANGE_EMAIL,
1436
1406
  loading: sendMutation.isPending,
1437
1407
  loadingLabel: L.SENDING,
1438
- onPress: form.handleSubmit(handleSubmit),
1439
- disabled: !form.formState.isValid
1408
+ onPress: form.handleSubmit(handleSubmit)
1440
1409
  }
1441
1410
  ),
1442
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
1411
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
1443
1412
  ] }) : /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
1444
1413
  /* @__PURE__ */ jsx(SuccessFeedback, { message: L.CHANGE_EMAIL_SENT }),
1445
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleResend, disabled: cooldown > 0, variant: "borderless", children: cooldown > 0 ? `${L.RESEND} (${cooldown}s)` : L.RESEND }) }),
1446
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
1414
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
1415
+ Button,
1416
+ {
1417
+ onPress: handleResend,
1418
+ label: cooldown > 0 ? `${L.RESEND} (${cooldown}s)` : L.RESEND
1419
+ }
1420
+ ) }),
1421
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center" }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
1447
1422
  ] })
1448
1423
  ] }) });
1449
1424
  }
@@ -1464,7 +1439,7 @@ function ChangePasswordScreen({ onSuccess }) {
1464
1439
  newPassword: data.newPassword
1465
1440
  });
1466
1441
  if (response.error) {
1467
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
1442
+ throw new RequestFailedError({ message: response.error.message ?? L.REQUEST_FAILED });
1468
1443
  }
1469
1444
  return response.data;
1470
1445
  }
@@ -1473,9 +1448,8 @@ function ChangePasswordScreen({ onSuccess }) {
1473
1448
  (data) => {
1474
1449
  changeMutation.mutate(data, {
1475
1450
  onSuccess: () => {
1451
+ form.reset();
1476
1452
  setShowSuccess(true);
1477
- form.handleSubmit(() => {
1478
- })();
1479
1453
  setTimeout(() => setShowSuccess(false), 3e3);
1480
1454
  onSuccess?.();
1481
1455
  }
@@ -1553,11 +1527,10 @@ function ChangePasswordScreen({ onSuccess }) {
1553
1527
  label: L.CHANGE_PASSWORD,
1554
1528
  loading: changeMutation.isPending,
1555
1529
  loadingLabel: L.CHANGING_PASSWORD,
1556
- onPress: form.handleSubmit(handleSubmit),
1557
- disabled: !form.formState.isValid
1530
+ onPress: form.handleSubmit(handleSubmit)
1558
1531
  }
1559
1532
  ),
1560
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
1533
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
1561
1534
  ] })
1562
1535
  ] }) });
1563
1536
  }
@@ -1575,7 +1548,7 @@ function DeleteAccountScreen({
1575
1548
  mutationFn: async (pw) => {
1576
1549
  const response = await client.deleteUser({ password: pw });
1577
1550
  if (response.error) {
1578
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
1551
+ throw new RequestFailedError({ message: response.error.message ?? L.REQUEST_FAILED });
1579
1552
  }
1580
1553
  return response.data;
1581
1554
  }
@@ -1643,27 +1616,25 @@ function DeleteAccountScreen({
1643
1616
  Button,
1644
1617
  {
1645
1618
  onPress: handleDelete,
1646
- disabled: !phraseMatches || !password || deleteMutation.isPending,
1647
- variant: "borderedProminent",
1648
- children: deleteMutation.isPending ? L.DELETING_ACCOUNT : L.DELETE
1619
+ label: deleteMutation.isPending ? L.DELETING_ACCOUNT : L.DELETE
1649
1620
  }
1650
1621
  ) }),
1651
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
1622
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
1652
1623
  ] })
1653
1624
  ] }) });
1654
1625
  }
1655
- function formatLastActive(dateStr) {
1626
+ function formatLastActive(dateStr, L) {
1656
1627
  if (!dateStr) return "";
1657
1628
  const date = new Date(dateStr);
1658
1629
  const now = /* @__PURE__ */ new Date();
1659
1630
  const diffMs = now.getTime() - date.getTime();
1660
1631
  const diffMin = Math.floor(diffMs / 6e4);
1661
- if (diffMin < 1) return "Just now";
1662
- if (diffMin < 60) return `${diffMin}m ago`;
1632
+ if (diffMin < 1) return L.JUST_NOW;
1633
+ if (diffMin < 60) return L.MINUTES_AGO.replace("{{n}}", String(diffMin));
1663
1634
  const diffHrs = Math.floor(diffMin / 60);
1664
- if (diffHrs < 24) return `${diffHrs}h ago`;
1635
+ if (diffHrs < 24) return L.HOURS_AGO.replace("{{n}}", String(diffHrs));
1665
1636
  const diffDays = Math.floor(diffHrs / 24);
1666
- return `${diffDays}d ago`;
1637
+ return L.DAYS_AGO.replace("{{n}}", String(diffDays));
1667
1638
  }
1668
1639
  function SessionsScreen({ onSuccess }) {
1669
1640
  const { localization: L } = useAuthLocalization();
@@ -1675,7 +1646,7 @@ function SessionsScreen({ onSuccess }) {
1675
1646
  queryFn: async () => {
1676
1647
  const response = await client.listSessions();
1677
1648
  if (response.error) {
1678
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
1649
+ throw new RequestFailedError({ message: response.error.message ?? L.REQUEST_FAILED });
1679
1650
  }
1680
1651
  return response.data ?? [];
1681
1652
  }
@@ -1684,7 +1655,7 @@ function SessionsScreen({ onSuccess }) {
1684
1655
  mutationFn: async (sessionToken) => {
1685
1656
  const response = await client.revokeSession({ token: sessionToken });
1686
1657
  if (response.error) {
1687
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
1658
+ throw new RequestFailedError({ message: response.error.message ?? L.REQUEST_FAILED });
1688
1659
  }
1689
1660
  return response.data;
1690
1661
  },
@@ -1696,7 +1667,7 @@ function SessionsScreen({ onSuccess }) {
1696
1667
  mutationFn: async () => {
1697
1668
  const response = await client.revokeOtherSessions();
1698
1669
  if (response.error) {
1699
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
1670
+ throw new RequestFailedError({ message: response.error.message ?? L.REQUEST_FAILED });
1700
1671
  }
1701
1672
  return response.data;
1702
1673
  },
@@ -1729,7 +1700,7 @@ function SessionsScreen({ onSuccess }) {
1729
1700
  ]);
1730
1701
  }, [L, revokeAllMutation]);
1731
1702
  const sessions = sessionsQuery.data ?? [];
1732
- const isPending = revokeMutation.isPending || revokeAllMutation.isPending;
1703
+ revokeMutation.isPending || revokeAllMutation.isPending;
1733
1704
  return /* @__PURE__ */ jsx(AuthContainer, { children: /* @__PURE__ */ jsxs(View, { style: { gap: 24, flex: 1 }, children: [
1734
1705
  /* @__PURE__ */ jsxs(View, { style: { gap: 4, alignItems: "center" }, children: [
1735
1706
  /* @__PURE__ */ jsx(Text, { style: { fontSize: 24, fontWeight: "700", color: "#1f2937" }, children: L.SESSIONS }),
@@ -1766,69 +1737,54 @@ function SessionsScreen({ onSuccess }) {
1766
1737
  /* @__PURE__ */ jsx(Text, { style: { fontSize: 16, fontWeight: "600", color: "#1f2937" }, children: item.userAgent ?? L.UNKNOWN }),
1767
1738
  /* @__PURE__ */ jsxs(Text, { style: { fontSize: 12, color: "#9ca3af" }, children: [
1768
1739
  item.ipAddress ?? "",
1769
- item.updatedAt ? ` \xB7 ${formatLastActive(item.updatedAt)}` : ""
1740
+ item.updatedAt ? ` \xB7 ${formatLastActive(item.updatedAt, L)}` : ""
1770
1741
  ] }),
1771
1742
  item.current && /* @__PURE__ */ jsx(Text, { style: { fontSize: 11, fontWeight: "600", color: "#16a34a" }, children: L.CURRENT_DEVICE })
1772
1743
  ] }),
1773
1744
  !item.current && /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
1774
1745
  Button,
1775
1746
  {
1776
- onPress: () => handleRevoke(item.token),
1777
- disabled: isPending,
1778
- variant: "bordered",
1779
- children: L.REVOKE_SESSION
1747
+ onPress: () => handleRevoke(item.token ?? item.id),
1748
+ label: L.REVOKE_SESSION
1780
1749
  }
1781
1750
  ) })
1782
1751
  ]
1783
1752
  }
1784
1753
  ),
1785
- ListFooterComponent: sessions.length > 1 ? /* @__PURE__ */ jsx(View, { style: { marginTop: 16 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleRevokeAll, disabled: isPending, variant: "bordered", children: L.REVOKE_ALL_OTHER_SESSIONS }) }) }) : null
1754
+ ListFooterComponent: sessions.length > 1 ? /* @__PURE__ */ jsx(View, { style: { marginTop: 16 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleRevokeAll, label: L.REVOKE_ALL_OTHER_SESSIONS }) }) }) : null
1786
1755
  }
1787
1756
  ),
1788
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
1757
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
1789
1758
  ] }) });
1790
1759
  }
1791
1760
  function TwoFactorSettingsScreen({ onSuccess }) {
1792
1761
  const { localization: L } = useAuthLocalization();
1793
1762
  const router = useRouter();
1794
- const client = useAuthClient();
1795
1763
  const { data: user } = useUser();
1796
1764
  const [state, setState] = useState({ step: "idle" });
1797
1765
  const [totpCode, setTotpCode] = useState("");
1798
1766
  const [password, setPassword] = useState("");
1799
1767
  const [copied, setCopied] = useState(false);
1800
1768
  const setupMutation = useMutation({
1801
- mutationFn: async () => {
1802
- const response = await client.twoFactor.enable({
1803
- password: "setup"
1804
- });
1805
- if (response.error) {
1806
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
1807
- }
1808
- return response.data;
1769
+ mutationFn: async (_pw) => {
1770
+ throw new NotImplementedError({ feature: "Two-factor setup" });
1809
1771
  }
1810
1772
  });
1811
1773
  const verifyMutation = useMutation({
1812
- mutationFn: async (code) => {
1813
- const response = await client.twoFactor.verifyTotp({ code });
1814
- if (response.error) {
1815
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
1816
- }
1817
- return response.data;
1774
+ mutationFn: async (_code) => {
1775
+ throw new NotImplementedError({ feature: "Two-factor verification" });
1818
1776
  }
1819
1777
  });
1820
1778
  const disableMutation = useMutation({
1821
- mutationFn: async (pw) => {
1822
- const response = await client.twoFactor.disable({ password: pw });
1823
- if (response.error) {
1824
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
1825
- }
1826
- return response.data;
1779
+ mutationFn: async (_pw) => {
1780
+ throw new NotImplementedError({ feature: "Two-factor disable" });
1827
1781
  }
1828
1782
  });
1829
1783
  const handleEnable = useCallback(() => {
1830
- setupMutation.mutate(void 0, {
1784
+ if (!password) return;
1785
+ setupMutation.mutate(password, {
1831
1786
  onSuccess: (data) => {
1787
+ setPassword("");
1832
1788
  setState({
1833
1789
  step: "setup",
1834
1790
  totpURI: data?.totpURI ?? "",
@@ -1836,7 +1792,7 @@ function TwoFactorSettingsScreen({ onSuccess }) {
1836
1792
  });
1837
1793
  }
1838
1794
  });
1839
- }, [setupMutation]);
1795
+ }, [password, setupMutation]);
1840
1796
  const handleVerify = useCallback(() => {
1841
1797
  if (totpCode.length !== 6) return;
1842
1798
  verifyMutation.mutate(totpCode, {
@@ -1876,7 +1832,7 @@ function TwoFactorSettingsScreen({ onSuccess }) {
1876
1832
  }
1877
1833
  });
1878
1834
  }, [password, disableMutation, onSuccess]);
1879
- const isEnabled = user?.twoFactorEnabled ?? false;
1835
+ const isEnabled = user?.twoFactorEnabled === true;
1880
1836
  const isPending = setupMutation.isPending || verifyMutation.isPending || disableMutation.isPending;
1881
1837
  const error = setupMutation.error ?? verifyMutation.error ?? disableMutation.error;
1882
1838
  return /* @__PURE__ */ jsx(AuthContainer, { children: /* @__PURE__ */ jsxs(ScrollView, { contentContainerStyle: { gap: 24 }, children: [
@@ -1890,11 +1846,61 @@ function TwoFactorSettingsScreen({ onSuccess }) {
1890
1846
  ": ",
1891
1847
  L.DISABLED
1892
1848
  ] }),
1893
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleEnable, disabled: isPending, variant: "borderedProminent", children: setupMutation.isPending ? L.LOADING : L.ENABLE_TWO_FACTOR }) })
1849
+ /* @__PURE__ */ jsx(Host, { matchContents: true, style: { opacity: isPending ? 0.5 : 1 }, children: /* @__PURE__ */ jsx(
1850
+ Button,
1851
+ {
1852
+ onPress: () => {
1853
+ if (isPending) return;
1854
+ setState({ step: "confirm-password" });
1855
+ },
1856
+ label: L.ENABLE_TWO_FACTOR
1857
+ }
1858
+ ) })
1859
+ ] }),
1860
+ state.step === "confirm-password" && /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
1861
+ /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, color: "#374151", textAlign: "center" }, children: L.PASSWORD }),
1862
+ /* @__PURE__ */ jsx(
1863
+ TextInput,
1864
+ {
1865
+ value: password,
1866
+ onChangeText: setPassword,
1867
+ placeholder: L.PASSWORD,
1868
+ secureTextEntry: true,
1869
+ editable: !setupMutation.isPending,
1870
+ style: {
1871
+ borderWidth: 1,
1872
+ borderColor: "#d1d5db",
1873
+ borderRadius: 8,
1874
+ padding: 12,
1875
+ fontSize: 16
1876
+ }
1877
+ }
1878
+ ),
1879
+ setupMutation.isPending && /* @__PURE__ */ jsx(ActivityIndicator, { size: "small" }),
1880
+ /* @__PURE__ */ jsx(Host, { matchContents: true, style: { opacity: setupMutation.isPending ? 0.5 : 1 }, children: /* @__PURE__ */ jsx(
1881
+ Button,
1882
+ {
1883
+ onPress: () => {
1884
+ if (setupMutation.isPending) return;
1885
+ handleEnable();
1886
+ },
1887
+ label: setupMutation.isPending ? L.LOADING : L.ENABLE_TWO_FACTOR
1888
+ }
1889
+ ) }),
1890
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
1891
+ Button,
1892
+ {
1893
+ onPress: () => {
1894
+ setState({ step: "idle" });
1895
+ setPassword("");
1896
+ },
1897
+ label: L.CANCEL
1898
+ }
1899
+ ) })
1894
1900
  ] }),
1895
1901
  state.step === "idle" && isEnabled && /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
1896
1902
  /* @__PURE__ */ jsx(SuccessFeedback, { message: `${L.STATUS}: ${L.ENABLED}` }),
1897
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleDisable, variant: "bordered", children: L.DISABLE_TWO_FACTOR }) })
1903
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleDisable, label: L.DISABLE_TWO_FACTOR }) })
1898
1904
  ] }),
1899
1905
  state.step === "setup" && /* @__PURE__ */ jsxs(View, { style: { gap: 16, alignItems: "center" }, children: [
1900
1906
  /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, color: "#374151", textAlign: "center" }, children: L.SCAN_QR_CODE }),
@@ -1909,7 +1915,7 @@ function TwoFactorSettingsScreen({ onSuccess }) {
1909
1915
  alignItems: "center",
1910
1916
  borderRadius: 8
1911
1917
  },
1912
- children: /* @__PURE__ */ jsx(Text, { style: { fontSize: 12, color: "#9ca3af" }, children: "QR Code" })
1918
+ children: /* @__PURE__ */ jsx(Text, { style: { fontSize: 12, color: "#9ca3af" }, children: L.SCAN_QR_CODE })
1913
1919
  }
1914
1920
  ),
1915
1921
  /* @__PURE__ */ jsxs(View, { style: { gap: 4, alignItems: "center" }, children: [
@@ -1936,8 +1942,7 @@ function TwoFactorSettingsScreen({ onSuccess }) {
1936
1942
  totpURI: state.totpURI,
1937
1943
  secret: state.secret
1938
1944
  }),
1939
- variant: "borderedProminent",
1940
- children: L.NEXT
1945
+ label: L.NEXT
1941
1946
  }
1942
1947
  ) })
1943
1948
  ] }),
@@ -1965,15 +1970,7 @@ function TwoFactorSettingsScreen({ onSuccess }) {
1965
1970
  }
1966
1971
  ),
1967
1972
  verifyMutation.isPending && /* @__PURE__ */ jsx(ActivityIndicator, { size: "small" }),
1968
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
1969
- Button,
1970
- {
1971
- onPress: handleVerify,
1972
- disabled: totpCode.length !== 6 || verifyMutation.isPending,
1973
- variant: "borderedProminent",
1974
- children: L.TWO_FACTOR_ACTION
1975
- }
1976
- ) })
1973
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleVerify, label: L.TWO_FACTOR_ACTION }) })
1977
1974
  ] }),
1978
1975
  state.step === "backup" && /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
1979
1976
  /* @__PURE__ */ jsx(SuccessFeedback, { message: L.TWO_FACTOR_SETUP_SUCCESS }),
@@ -2002,8 +1999,14 @@ function TwoFactorSettingsScreen({ onSuccess }) {
2002
1999
  ))
2003
2000
  }
2004
2001
  ),
2005
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleCopyBackupCodes, variant: "bordered", children: copied ? L.BACKUP_CODES_COPIED : L.COPY_BACKUP_CODES }) }),
2006
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => setState({ step: "idle" }), variant: "borderedProminent", children: L.FINISH }) })
2002
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
2003
+ Button,
2004
+ {
2005
+ onPress: handleCopyBackupCodes,
2006
+ label: copied ? L.BACKUP_CODES_COPIED : L.COPY_BACKUP_CODES
2007
+ }
2008
+ ) }),
2009
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => setState({ step: "idle" }), label: L.FINISH }) })
2007
2010
  ] }),
2008
2011
  state.step === "disabling" && /* @__PURE__ */ jsxs(View, { style: { gap: 16 }, children: [
2009
2012
  /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, color: "#374151", textAlign: "center" }, children: L.CONFIRM_DISABLE_2FA }),
@@ -2025,19 +2028,11 @@ function TwoFactorSettingsScreen({ onSuccess }) {
2025
2028
  }
2026
2029
  ),
2027
2030
  disableMutation.isPending && /* @__PURE__ */ jsx(ActivityIndicator, { size: "small" }),
2028
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
2029
- Button,
2030
- {
2031
- onPress: handleConfirmDisable,
2032
- disabled: !password || disableMutation.isPending,
2033
- variant: "borderedProminent",
2034
- children: L.DISABLE_TWO_FACTOR
2035
- }
2036
- ) }),
2037
- /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => setState({ step: "idle" }), variant: "borderless", children: L.CANCEL }) })
2031
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleConfirmDisable, label: L.DISABLE_TWO_FACTOR }) }),
2032
+ /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => setState({ step: "idle" }), label: L.CANCEL }) })
2038
2033
  ] }),
2039
2034
  error && /* @__PURE__ */ jsx(ErrorFeedback, { message: error.message }),
2040
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
2035
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
2041
2036
  ] }) });
2042
2037
  }
2043
2038
  var AVATAR_SIZE = 120;
@@ -2056,13 +2051,12 @@ function UpdateAvatarScreen({ onSuccess }) {
2056
2051
  const queryClient = useQueryClient();
2057
2052
  const { data: user } = useUser();
2058
2053
  const [selectedUri, setSelectedUri] = useState(null);
2059
- const [showSuccess, setShowSuccess] = useState(false);
2060
- const [successMessage, setSuccessMessage] = useState("");
2054
+ const [successMessage, setSuccessMessage] = useState(null);
2061
2055
  const uploadMutation = useMutation({
2062
2056
  mutationFn: async (imageUri) => {
2063
2057
  const response = await client.updateUser({ image: imageUri });
2064
2058
  if (response.error) {
2065
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
2059
+ throw new RequestFailedError({ message: response.error.message ?? L.REQUEST_FAILED });
2066
2060
  }
2067
2061
  return response.data;
2068
2062
  }
@@ -2071,43 +2065,57 @@ function UpdateAvatarScreen({ onSuccess }) {
2071
2065
  mutationFn: async () => {
2072
2066
  const response = await client.updateUser({ image: "" });
2073
2067
  if (response.error) {
2074
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
2068
+ throw new RequestFailedError({ message: response.error.message ?? L.REQUEST_FAILED });
2075
2069
  }
2076
2070
  return response.data;
2077
2071
  }
2078
2072
  });
2073
+ const handlePickImage = useCallback(async () => {
2074
+ const result = await ImagePicker.launchImageLibraryAsync({
2075
+ mediaTypes: "images",
2076
+ allowsEditing: true,
2077
+ aspect: [1, 1],
2078
+ quality: 0.8
2079
+ });
2080
+ if (!result.canceled && result.assets[0]) {
2081
+ setSelectedUri(result.assets[0].uri);
2082
+ }
2083
+ }, []);
2084
+ const handleDelete = useCallback(() => {
2085
+ deleteMutation.mutate(void 0, {
2086
+ onSuccess: () => {
2087
+ queryClient.invalidateQueries({ queryKey: authKeys.user() });
2088
+ setSuccessMessage(L.AVATAR_DELETED);
2089
+ setTimeout(() => setSuccessMessage(null), 3e3);
2090
+ onSuccess?.();
2091
+ }
2092
+ });
2093
+ }, [deleteMutation, queryClient, L, onSuccess]);
2079
2094
  const showActionSheet = useCallback(() => {
2080
- ActionSheetIOS.showActionSheetWithOptions(
2081
- {
2082
- options: [L.CANCEL, L.CLICK_TO_UPLOAD, L.DELETE_AVATAR],
2083
- cancelButtonIndex: 0,
2084
- destructiveButtonIndex: 2
2085
- },
2086
- async (buttonIndex) => {
2087
- if (buttonIndex === 1) {
2088
- const result = await ImagePicker.launchImageLibraryAsync({
2089
- mediaTypes: "images",
2090
- allowsEditing: true,
2091
- aspect: [1, 1],
2092
- quality: 0.8
2093
- });
2094
- if (!result.canceled && result.assets[0]) {
2095
- setSelectedUri(result.assets[0].uri);
2096
- }
2097
- } else if (buttonIndex === 2) {
2098
- deleteMutation.mutate(void 0, {
2099
- onSuccess: () => {
2100
- queryClient.invalidateQueries({ queryKey: authKeys.user() });
2101
- setSuccessMessage(L.AVATAR_DELETED);
2102
- setShowSuccess(true);
2103
- setTimeout(() => setShowSuccess(false), 3e3);
2104
- onSuccess?.();
2105
- }
2106
- });
2107
- }
2095
+ const handleAction = (buttonIndex) => {
2096
+ if (buttonIndex === 1) {
2097
+ handlePickImage();
2098
+ } else if (buttonIndex === 2) {
2099
+ handleDelete();
2108
2100
  }
2109
- );
2110
- }, [L, deleteMutation, queryClient, onSuccess]);
2101
+ };
2102
+ if (Platform.OS === "ios") {
2103
+ ActionSheetIOS.showActionSheetWithOptions(
2104
+ {
2105
+ options: [L.CANCEL, L.CLICK_TO_UPLOAD, L.DELETE_AVATAR],
2106
+ cancelButtonIndex: 0,
2107
+ destructiveButtonIndex: 2
2108
+ },
2109
+ handleAction
2110
+ );
2111
+ } else {
2112
+ Alert.alert(L.AVATAR, void 0, [
2113
+ { text: L.CLICK_TO_UPLOAD, onPress: () => handleAction(1) },
2114
+ { text: L.DELETE_AVATAR, style: "destructive", onPress: () => handleAction(2) },
2115
+ { text: L.CANCEL, style: "cancel" }
2116
+ ]);
2117
+ }
2118
+ }, [L, handlePickImage, handleDelete]);
2111
2119
  const handleUpload = useCallback(() => {
2112
2120
  if (!selectedUri) return;
2113
2121
  uploadMutation.mutate(selectedUri, {
@@ -2115,8 +2123,7 @@ function UpdateAvatarScreen({ onSuccess }) {
2115
2123
  queryClient.invalidateQueries({ queryKey: authKeys.user() });
2116
2124
  setSelectedUri(null);
2117
2125
  setSuccessMessage(L.AVATAR_UPDATED);
2118
- setShowSuccess(true);
2119
- setTimeout(() => setShowSuccess(false), 3e3);
2126
+ setTimeout(() => setSuccessMessage(null), 3e3);
2120
2127
  onSuccess?.();
2121
2128
  }
2122
2129
  });
@@ -2129,7 +2136,7 @@ function UpdateAvatarScreen({ onSuccess }) {
2129
2136
  /* @__PURE__ */ jsx(Text, { style: { fontSize: 24, fontWeight: "700", color: "#1f2937" }, children: L.AVATAR }),
2130
2137
  /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, color: "#6b7280" }, children: L.AVATAR_DESCRIPTION })
2131
2138
  ] }),
2132
- showSuccess ? /* @__PURE__ */ jsx(SuccessFeedback, { message: successMessage }) : /* @__PURE__ */ jsxs(View, { style: { gap: 16, alignItems: "center" }, children: [
2139
+ successMessage ? /* @__PURE__ */ jsx(SuccessFeedback, { message: successMessage }) : /* @__PURE__ */ jsxs(View, { style: { gap: 16, alignItems: "center" }, children: [
2133
2140
  /* @__PURE__ */ jsx(Pressable, { onPress: showActionSheet, disabled: isPending, children: displayUri ? /* @__PURE__ */ jsx(
2134
2141
  Image,
2135
2142
  {
@@ -2155,9 +2162,9 @@ function UpdateAvatarScreen({ onSuccess }) {
2155
2162
  }
2156
2163
  ) }),
2157
2164
  isPending && /* @__PURE__ */ jsx(ActivityIndicator, { size: "small" }),
2158
- selectedUri && /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleUpload, disabled: isPending, variant: "borderedProminent", children: L.UPLOAD_AVATAR }) }),
2165
+ selectedUri && /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: handleUpload, label: L.UPLOAD_AVATAR }) }),
2159
2166
  error && /* @__PURE__ */ jsx(ErrorFeedback, { message: error.message }),
2160
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
2167
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
2161
2168
  ] })
2162
2169
  ] }) });
2163
2170
  }
@@ -2177,7 +2184,7 @@ function UpdateNameScreen({ onSuccess }) {
2177
2184
  mutationFn: async (data) => {
2178
2185
  const response = await client.updateUser({ name: data.name });
2179
2186
  if (response.error) {
2180
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
2187
+ throw new RequestFailedError({ message: response.error.message ?? L.REQUEST_FAILED });
2181
2188
  }
2182
2189
  return response.data;
2183
2190
  }
@@ -2227,82 +2234,31 @@ function UpdateNameScreen({ onSuccess }) {
2227
2234
  label: L.SAVE,
2228
2235
  loading: updateMutation.isPending,
2229
2236
  loadingLabel: L.LOADING,
2230
- onPress: form.handleSubmit(handleSubmit),
2231
- disabled: !form.formState.isValid
2237
+ onPress: form.handleSubmit(handleSubmit)
2232
2238
  }
2233
2239
  ),
2234
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
2240
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
2235
2241
  ] })
2236
2242
  ] }) });
2237
2243
  }
2238
2244
  function UpdateUsernameScreen({ onSuccess }) {
2239
2245
  const { localization: L } = useAuthLocalization();
2240
2246
  const router = useRouter();
2241
- const client = useAuthClient();
2247
+ useAuthClient();
2242
2248
  const queryClient = useQueryClient();
2243
- const { data: user } = useUser();
2249
+ const { data: _user } = useUser();
2244
2250
  const [showSuccess, setShowSuccess] = useState(false);
2245
- const [availability, setAvailability] = useState("idle");
2246
- const debounceRef = useRef(null);
2247
2251
  const form = useForm({
2248
2252
  resolver: zodResolver(updateUsernameSchema),
2249
- defaultValues: { username: user?.username ?? "" },
2253
+ // BA 1.5: username not available on user object
2254
+ defaultValues: { username: "" },
2250
2255
  mode: "onChange"
2251
2256
  });
2252
2257
  const updateMutation = useMutation({
2253
- mutationFn: async (data) => {
2254
- const response = await client.updateUser({
2255
- username: data.username
2256
- });
2257
- if (response.error) {
2258
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
2259
- }
2260
- return response.data;
2258
+ mutationFn: async (_data) => {
2259
+ throw new NotImplementedError({ feature: "Username updates" });
2261
2260
  }
2262
2261
  });
2263
- const checkAvailability = useCallback(
2264
- async (username) => {
2265
- if (!username || username === user?.username) {
2266
- setAvailability("idle");
2267
- return;
2268
- }
2269
- setAvailability("checking");
2270
- try {
2271
- const response = await client.getSession();
2272
- if (response.error) {
2273
- setAvailability("idle");
2274
- } else {
2275
- setAvailability("available");
2276
- }
2277
- } catch {
2278
- setAvailability("idle");
2279
- }
2280
- },
2281
- [client, user?.username]
2282
- );
2283
- const handleUsernameChange = useCallback(
2284
- (value) => {
2285
- if (debounceRef.current) {
2286
- clearTimeout(debounceRef.current);
2287
- }
2288
- if (!value || value === user?.username) {
2289
- setAvailability("idle");
2290
- return;
2291
- }
2292
- setAvailability("checking");
2293
- debounceRef.current = setTimeout(() => {
2294
- checkAvailability(value);
2295
- }, 300);
2296
- },
2297
- [checkAvailability, user?.username]
2298
- );
2299
- useEffect(() => {
2300
- return () => {
2301
- if (debounceRef.current) {
2302
- clearTimeout(debounceRef.current);
2303
- }
2304
- };
2305
- }, []);
2306
2262
  const handleSubmit = useCallback(
2307
2263
  (data) => {
2308
2264
  updateMutation.mutate(data, {
@@ -2327,30 +2283,19 @@ function UpdateUsernameScreen({ onSuccess }) {
2327
2283
  {
2328
2284
  control: form.control,
2329
2285
  name: "username",
2330
- render: ({ field: { onChange, onBlur, value }, fieldState: { error } }) => /* @__PURE__ */ jsxs(View, { style: { gap: 4 }, children: [
2331
- /* @__PURE__ */ jsx(
2332
- FormInput,
2333
- {
2334
- label: L.USERNAME,
2335
- placeholder: L.USERNAME_PLACEHOLDER,
2336
- autoCapitalize: "none",
2337
- editable: !updateMutation.isPending,
2338
- onChangeText: (text) => {
2339
- onChange(text);
2340
- handleUsernameChange(text);
2341
- },
2342
- onBlur,
2343
- value,
2344
- error: error?.message
2345
- }
2346
- ),
2347
- availability === "checking" && /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 4 }, children: [
2348
- /* @__PURE__ */ jsx(ActivityIndicator, { size: "small" }),
2349
- /* @__PURE__ */ jsx(Text, { style: { fontSize: 12, color: "#6b7280" }, children: L.CHECKING_AVAILABILITY })
2350
- ] }),
2351
- availability === "available" && /* @__PURE__ */ jsx(Text, { style: { fontSize: 12, color: "#16a34a" }, children: L.USERNAME_AVAILABLE_STATUS }),
2352
- availability === "taken" && /* @__PURE__ */ jsx(Text, { style: { fontSize: 12, color: "#dc2626" }, children: L.USERNAME_TAKEN_STATUS })
2353
- ] })
2286
+ render: ({ field: { onChange, onBlur, value }, fieldState: { error } }) => /* @__PURE__ */ jsx(
2287
+ FormInput,
2288
+ {
2289
+ label: L.USERNAME,
2290
+ placeholder: L.USERNAME_PLACEHOLDER,
2291
+ autoCapitalize: "none",
2292
+ editable: !updateMutation.isPending,
2293
+ onChangeText: onChange,
2294
+ onBlur,
2295
+ value,
2296
+ error: error?.message
2297
+ }
2298
+ )
2354
2299
  }
2355
2300
  ),
2356
2301
  updateMutation.error && /* @__PURE__ */ jsx(ErrorFeedback, { message: updateMutation.error.message }),
@@ -2360,11 +2305,10 @@ function UpdateUsernameScreen({ onSuccess }) {
2360
2305
  label: L.SAVE,
2361
2306
  loading: updateMutation.isPending,
2362
2307
  loadingLabel: L.LOADING,
2363
- onPress: form.handleSubmit(handleSubmit),
2364
- disabled: !form.formState.isValid || availability === "taken" || availability === "checking"
2308
+ onPress: form.handleSubmit(handleSubmit)
2365
2309
  }
2366
2310
  ),
2367
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
2311
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
2368
2312
  ] })
2369
2313
  ] }) });
2370
2314
  }
@@ -2375,32 +2319,18 @@ function OrganizationMembersScreen({
2375
2319
  }) {
2376
2320
  const { localization: L } = useAuthLocalization();
2377
2321
  const router = useRouter();
2378
- const client = useAuthClient();
2322
+ useAuthClient();
2379
2323
  const queryClient = useQueryClient();
2380
2324
  const [searchQuery, setSearchQuery] = useState("");
2381
2325
  const membersQuery = useQuery({
2382
2326
  queryKey: ["organizations", organizationId, "members"],
2383
2327
  queryFn: async () => {
2384
- const response = await client.organization.getFullOrganization({
2385
- query: { organizationId }
2386
- });
2387
- if (response.error) {
2388
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
2389
- }
2390
- return response.data?.members ?? [];
2328
+ throw new NotImplementedError({ feature: "Organization member management" });
2391
2329
  }
2392
2330
  });
2393
2331
  const updateRoleMutation = useMutation({
2394
- mutationFn: async ({ memberId, role }) => {
2395
- const response = await client.organization.updateMemberRole({
2396
- organizationId,
2397
- memberId,
2398
- role
2399
- });
2400
- if (response.error) {
2401
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
2402
- }
2403
- return response.data;
2332
+ mutationFn: async (_memberId_role) => {
2333
+ throw new NotImplementedError({ feature: "Organization role updates" });
2404
2334
  },
2405
2335
  onSuccess: () => {
2406
2336
  queryClient.invalidateQueries({
@@ -2409,15 +2339,8 @@ function OrganizationMembersScreen({
2409
2339
  }
2410
2340
  });
2411
2341
  const removeMutation = useMutation({
2412
- mutationFn: async (memberId) => {
2413
- const response = await client.organization.removeMember({
2414
- organizationId,
2415
- memberIdOrEmail: memberId
2416
- });
2417
- if (response.error) {
2418
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
2419
- }
2420
- return response.data;
2342
+ mutationFn: async (_memberId) => {
2343
+ throw new NotImplementedError({ feature: "Organization member removal" });
2421
2344
  },
2422
2345
  onSuccess: () => {
2423
2346
  queryClient.invalidateQueries({
@@ -2426,49 +2349,63 @@ function OrganizationMembersScreen({
2426
2349
  onSuccess?.();
2427
2350
  }
2428
2351
  });
2352
+ const showMemberActionSheet = useCallback(
2353
+ (callback) => {
2354
+ const options = [L.ADMIN, L.MEMBER, L.REMOVE_MEMBER, L.CANCEL];
2355
+ if (Platform.OS === "ios") {
2356
+ ActionSheetIOS.showActionSheetWithOptions(
2357
+ {
2358
+ options,
2359
+ cancelButtonIndex: 3,
2360
+ destructiveButtonIndex: 2
2361
+ },
2362
+ callback
2363
+ );
2364
+ } else {
2365
+ Alert.alert(L.MANAGE_ROLE, void 0, [
2366
+ { text: L.ADMIN, onPress: () => callback(0) },
2367
+ { text: L.MEMBER, onPress: () => callback(1) },
2368
+ { text: L.REMOVE_MEMBER, style: "destructive", onPress: () => callback(2) },
2369
+ { text: L.CANCEL, style: "cancel" }
2370
+ ]);
2371
+ }
2372
+ },
2373
+ [L]
2374
+ );
2429
2375
  const handleMemberAction = useCallback(
2430
- // biome-ignore lint/suspicious/noExplicitAny: member type from Better Auth
2431
2376
  (member) => {
2432
2377
  if (!isAdmin) return;
2433
- const roles = [L.ADMIN, L.MEMBER, L.REMOVE_MEMBER, L.CANCEL];
2434
- ActionSheetIOS.showActionSheetWithOptions(
2435
- {
2436
- options: roles,
2437
- cancelButtonIndex: 3,
2438
- destructiveButtonIndex: 2
2439
- },
2440
- (buttonIndex) => {
2441
- const roleMap = { 0: "admin", 1: "member" };
2442
- if (buttonIndex === 2) {
2443
- Alert.alert(L.REMOVE_MEMBER, L.CONFIRM_REMOVE_MEMBER, [
2444
- { text: L.CANCEL, style: "cancel" },
2445
- {
2446
- text: L.REMOVE,
2447
- style: "destructive",
2448
- onPress: () => removeMutation.mutate(member.id)
2449
- }
2450
- ]);
2451
- } else if (roleMap[buttonIndex]) {
2452
- updateRoleMutation.mutate({
2453
- memberId: member.id,
2454
- role: roleMap[buttonIndex]
2455
- });
2456
- }
2378
+ showMemberActionSheet((buttonIndex) => {
2379
+ const roleMap = { 0: "admin", 1: "member" };
2380
+ if (buttonIndex === 2) {
2381
+ Alert.alert(L.REMOVE_MEMBER, L.CONFIRM_REMOVE_MEMBER, [
2382
+ { text: L.CANCEL, style: "cancel" },
2383
+ {
2384
+ text: L.REMOVE,
2385
+ style: "destructive",
2386
+ onPress: () => removeMutation.mutate(member.id)
2387
+ }
2388
+ ]);
2389
+ } else if (roleMap[buttonIndex]) {
2390
+ updateRoleMutation.mutate({
2391
+ memberId: member.id,
2392
+ role: roleMap[buttonIndex]
2393
+ });
2457
2394
  }
2458
- );
2395
+ });
2459
2396
  },
2460
- [isAdmin, L, removeMutation, updateRoleMutation]
2397
+ [isAdmin, L, removeMutation, updateRoleMutation, showMemberActionSheet]
2461
2398
  );
2462
2399
  const allMembers = membersQuery.data ?? [];
2463
2400
  const filteredMembers = searchQuery ? allMembers.filter(
2464
2401
  (m) => (m.user?.name ?? "").toLowerCase().includes(searchQuery.toLowerCase()) || (m.user?.email ?? "").toLowerCase().includes(searchQuery.toLowerCase())
2465
2402
  ) : allMembers;
2466
- const isPending = updateRoleMutation.isPending || removeMutation.isPending;
2403
+ updateRoleMutation.isPending || removeMutation.isPending;
2467
2404
  const error = updateRoleMutation.error ?? removeMutation.error;
2468
2405
  return /* @__PURE__ */ jsx(AuthContainer, { children: /* @__PURE__ */ jsxs(View, { style: { gap: 24, flex: 1 }, children: [
2469
2406
  /* @__PURE__ */ jsxs(View, { style: { gap: 4, alignItems: "center" }, children: [
2470
2407
  /* @__PURE__ */ jsx(Text, { style: { fontSize: 24, fontWeight: "700", color: "#1f2937" }, children: L.MEMBERS }),
2471
- /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, color: "#6b7280" }, children: L.LOADING_MEMBERS.replace("Loading members...", "Manage organization members") })
2408
+ /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, color: "#6b7280" }, children: L.MEMBERS_SUBTITLE })
2472
2409
  ] }),
2473
2410
  allMembers.length > 10 && /* @__PURE__ */ jsx(
2474
2411
  TextInput,
@@ -2537,15 +2474,7 @@ function OrganizationMembersScreen({
2537
2474
  item.role
2538
2475
  ] })
2539
2476
  ] }),
2540
- isAdmin && !isOwner && /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
2541
- Button,
2542
- {
2543
- onPress: () => handleMemberAction(item),
2544
- disabled: isPending,
2545
- variant: "bordered",
2546
- children: L.ROLE
2547
- }
2548
- ) })
2477
+ isAdmin && !isOwner && /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => handleMemberAction(item), label: L.MANAGE_ROLE }) })
2549
2478
  ]
2550
2479
  }
2551
2480
  );
@@ -2553,7 +2482,7 @@ function OrganizationMembersScreen({
2553
2482
  }
2554
2483
  ),
2555
2484
  error && /* @__PURE__ */ jsx(ErrorFeedback, { message: error.message }),
2556
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
2485
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
2557
2486
  ] }) });
2558
2487
  }
2559
2488
  function OrganizationSwitcherScreen({
@@ -2562,25 +2491,17 @@ function OrganizationSwitcherScreen({
2562
2491
  }) {
2563
2492
  const { localization: L } = useAuthLocalization();
2564
2493
  const router = useRouter();
2565
- const client = useAuthClient();
2494
+ useAuthClient();
2566
2495
  const [searchQuery, setSearchQuery] = useState("");
2567
2496
  const orgsQuery = useQuery({
2568
2497
  queryKey: ["organizations", "list"],
2569
2498
  queryFn: async () => {
2570
- const response = await client.organization.list();
2571
- if (response.error) {
2572
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
2573
- }
2574
- return response.data ?? [];
2499
+ throw new NotImplementedError({ feature: "Organization listing" });
2575
2500
  }
2576
2501
  });
2577
2502
  const setActiveMutation = useMutation({
2578
- mutationFn: async (orgId) => {
2579
- const response = await client.organization.setActive({ organizationId: orgId });
2580
- if (response.error) {
2581
- throw new Error(response.error.message ?? L.REQUEST_FAILED);
2582
- }
2583
- return response.data;
2503
+ mutationFn: async (_orgId) => {
2504
+ throw new NotImplementedError({ feature: "Organization switching" });
2584
2505
  }
2585
2506
  });
2586
2507
  const handleSelect = useCallback(
@@ -2660,22 +2581,14 @@ function OrganizationSwitcherScreen({
2660
2581
  ),
2661
2582
  item.members && /* @__PURE__ */ jsx(Text, { style: { fontSize: 12, color: "#9ca3af" }, children: L.MEMBER_COUNT.replace("{{count}}", String(item.members.length ?? 0)) })
2662
2583
  ] }),
2663
- isCurrent ? /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, color: "#16a34a", fontWeight: "600" }, children: "\u2713" }) : /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(
2664
- Button,
2665
- {
2666
- onPress: () => handleSelect(item.id),
2667
- disabled: setActiveMutation.isPending,
2668
- variant: "bordered",
2669
- children: L.SELECT_ORGANIZATION
2670
- }
2671
- ) })
2584
+ isCurrent ? /* @__PURE__ */ jsx(Text, { style: { fontSize: 14, color: "#16a34a", fontWeight: "600" }, children: "\u2713" }) : /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => handleSelect(item.id), label: L.SELECT_ORGANIZATION }) })
2672
2585
  ]
2673
2586
  }
2674
2587
  );
2675
2588
  }
2676
2589
  }
2677
2590
  ),
2678
- /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), variant: "borderless", children: L.GO_BACK }) }) })
2591
+ /* @__PURE__ */ jsx(View, { style: { alignItems: "center", marginTop: 8 }, children: /* @__PURE__ */ jsx(Host, { matchContents: true, children: /* @__PURE__ */ jsx(Button, { onPress: () => router.back(), label: L.GO_BACK }) }) })
2679
2592
  ] }) });
2680
2593
  }
2681
2594
  function useAuthRedirect() {
@@ -2717,7 +2630,7 @@ function useBiometric() {
2717
2630
  cancelLabel: "Cancel"
2718
2631
  });
2719
2632
  if (!result.success) {
2720
- throw new Error(result.error ?? "BIOMETRIC_FAILED");
2633
+ throw new RequestFailedError({ message: result.error ?? "BIOMETRIC_FAILED_ERROR" });
2721
2634
  }
2722
2635
  return result;
2723
2636
  }
@@ -2731,7 +2644,7 @@ function useLogout() {
2731
2644
  mutationFn: async () => {
2732
2645
  const response = await client.signOut();
2733
2646
  if (response.error) {
2734
- throw new Error(response.error.message ?? "Logout failed");
2647
+ throw new RequestFailedError({ message: response.error.message ?? "Logout failed" });
2735
2648
  }
2736
2649
  return response.data;
2737
2650
  },