@erikey/react 0.5.2 → 0.6.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.
@@ -65,7 +65,8 @@ function EmailForm({
65
65
  authClient,
66
66
  localization: contextLocalization,
67
67
  toast,
68
- localizeErrors
68
+ localizeErrors,
69
+ onAuthEvent
69
70
  } = useContext(AuthUIContext)
70
71
 
71
72
  localization = { ...contextLocalization, ...localization }
@@ -90,6 +91,9 @@ function EmailForm({
90
91
  }, [form.formState.isSubmitting, setIsSubmitting])
91
92
 
92
93
  async function sendEmailOTP({ email }: z.infer<typeof formSchema>) {
94
+ // Emit start event
95
+ onAuthEvent?.({ type: "SIGN_IN_START", email })
96
+
93
97
  const fetchOptions: BetterFetchOption = {
94
98
  throw: true,
95
99
  headers: await getCaptchaHeaders("/email-otp/send-verification-otp")
@@ -109,13 +113,22 @@ function EmailForm({
109
113
 
110
114
  setEmail(email)
111
115
  } catch (error) {
116
+ const errorMessage = getLocalizedError({
117
+ error,
118
+ localization,
119
+ localizeErrors
120
+ })
121
+ const errorCode = (error as { error?: { code?: string } })?.error
122
+ ?.code
123
+
124
+ onAuthEvent?.({
125
+ type: "SIGN_IN_ERROR",
126
+ error: { message: errorMessage, code: errorCode }
127
+ })
128
+
112
129
  toast({
113
130
  variant: "error",
114
- message: getLocalizedError({
115
- error,
116
- localization,
117
- localizeErrors
118
- })
131
+ message: errorMessage
119
132
  })
120
133
  }
121
134
  }
@@ -193,7 +206,8 @@ export function OTPForm({
193
206
  authClient,
194
207
  localization: contextLocalization,
195
208
  toast,
196
- localizeErrors
209
+ localizeErrors,
210
+ onAuthEvent
197
211
  } = useContext(AuthUIContext)
198
212
 
199
213
  localization = { ...contextLocalization, ...localization }
@@ -229,21 +243,47 @@ export function OTPForm({
229
243
 
230
244
  async function verifyCode({ code }: z.infer<typeof formSchema>) {
231
245
  try {
232
- await authClient.signIn.emailOtp({
246
+ const data = await authClient.signIn.emailOtp({
233
247
  email,
234
248
  otp: code,
235
249
  fetchOptions: { throw: true }
236
250
  })
237
251
 
252
+ // Build user and session for events
253
+ const user = (data as { user?: Record<string, unknown> }).user as {
254
+ id: string
255
+ email: string
256
+ name?: string | null
257
+ image?: string | null
258
+ emailVerified: boolean
259
+ }
260
+ const session = {
261
+ token: (data as { token?: string }).token,
262
+ user
263
+ }
264
+
265
+ // Emit success events
266
+ onAuthEvent?.({ type: "SIGN_IN_SUCCESS", user, session })
267
+ onAuthEvent?.({ type: "AUTH_SUCCESS", user, session })
268
+
238
269
  await onSuccess()
239
270
  } catch (error) {
271
+ const errorMessage = getLocalizedError({
272
+ error,
273
+ localization,
274
+ localizeErrors
275
+ })
276
+ const errorCode = (error as { error?: { code?: string } })?.error
277
+ ?.code
278
+
279
+ onAuthEvent?.({
280
+ type: "SIGN_IN_ERROR",
281
+ error: { message: errorMessage, code: errorCode }
282
+ })
283
+
240
284
  toast({
241
285
  variant: "error",
242
- message: getLocalizedError({
243
- error,
244
- localization,
245
- localizeErrors
246
- })
286
+ message: errorMessage
247
287
  })
248
288
 
249
289
  form.reset()
@@ -273,7 +313,11 @@ export function OTPForm({
273
313
  field.onChange(value)
274
314
 
275
315
  if (value.length === 6) {
276
- form.handleSubmit(verifyCode)()
316
+ form.handleSubmit(verifyCode)().catch(
317
+ () => {
318
+ // Error already handled in verifyCode
319
+ }
320
+ )
277
321
  }
278
322
  }}
279
323
  containerClassName={
@@ -354,7 +354,11 @@ export function EmailVerificationForm({
354
354
  field.onChange(value)
355
355
 
356
356
  if (value.length === 6) {
357
- form.handleSubmit(verifyCode)()
357
+ form.handleSubmit(verifyCode)().catch(
358
+ () => {
359
+ // Error already handled in verifyCode
360
+ }
361
+ )
358
362
  }
359
363
  }}
360
364
  containerClassName={
@@ -58,7 +58,8 @@ export function MagicLinkForm({
58
58
  redirectTo: contextRedirectTo,
59
59
  viewPaths,
60
60
  toast,
61
- localizeErrors
61
+ localizeErrors,
62
+ onAuthEvent
62
63
  } = useContext(AuthUIContext)
63
64
 
64
65
  localization = { ...contextLocalization, ...localization }
@@ -107,6 +108,9 @@ export function MagicLinkForm({
107
108
  }, [form.formState.isSubmitting, setIsSubmitting])
108
109
 
109
110
  async function sendMagicLink({ email }: z.infer<typeof formSchema>) {
111
+ // Emit start event
112
+ onAuthEvent?.({ type: "SIGN_IN_START", email })
113
+
110
114
  try {
111
115
  const fetchOptions: BetterFetchOption = {
112
116
  throw: true,
@@ -126,13 +130,22 @@ export function MagicLinkForm({
126
130
 
127
131
  form.reset()
128
132
  } catch (error) {
133
+ const errorMessage = getLocalizedError({
134
+ error,
135
+ localization,
136
+ localizeErrors
137
+ })
138
+ const errorCode = (error as { error?: { code?: string } })?.error
139
+ ?.code
140
+
141
+ onAuthEvent?.({
142
+ type: "SIGN_IN_ERROR",
143
+ error: { message: errorMessage, code: errorCode }
144
+ })
145
+
129
146
  toast({
130
147
  variant: "error",
131
- message: getLocalizedError({
132
- error,
133
- localization,
134
- localizeErrors
135
- })
148
+ message: errorMessage
136
149
  })
137
150
  resetCaptcha()
138
151
  }
@@ -268,11 +268,21 @@ export function SignUpForm({
268
268
  })
269
269
 
270
270
  isSubmitting =
271
- isSubmitting || form.formState.isSubmitting || transitionPending
271
+ isSubmitting ||
272
+ form.formState.isSubmitting ||
273
+ transitionPending ||
274
+ uploadingAvatar
272
275
 
273
276
  useEffect(() => {
274
- setIsSubmitting?.(form.formState.isSubmitting || transitionPending)
275
- }, [form.formState.isSubmitting, transitionPending, setIsSubmitting])
277
+ setIsSubmitting?.(
278
+ form.formState.isSubmitting || transitionPending || uploadingAvatar
279
+ )
280
+ }, [
281
+ form.formState.isSubmitting,
282
+ transitionPending,
283
+ uploadingAvatar,
284
+ setIsSubmitting
285
+ ])
276
286
 
277
287
  const handleAvatarChange = async (file: File) => {
278
288
  if (!avatar) return
@@ -54,6 +54,7 @@ export function TwoFactorForm({
54
54
 
55
55
  const {
56
56
  authClient,
57
+ authFlowMode,
57
58
  basePath,
58
59
  hooks: { useSession },
59
60
  localization: contextLocalization,
@@ -156,7 +157,11 @@ export function TwoFactorForm({
156
157
  (error as BetterFetchError).error.code ===
157
158
  "INVALID_TWO_FACTOR_COOKIE"
158
159
  ) {
159
- history.back()
160
+ // In internal mode, AuthFlow handles navigation via events
161
+ // In route mode, use browser history
162
+ if (authFlowMode !== "internal") {
163
+ history.back()
164
+ }
160
165
  }
161
166
  }
162
167
 
@@ -291,7 +296,9 @@ export function TwoFactorForm({
291
296
  if (value.length === 6) {
292
297
  form.handleSubmit(
293
298
  verifyCode
294
- )()
299
+ )().catch(() => {
300
+ // Error already handled in verifyCode
301
+ })
295
302
  }
296
303
  }}
297
304
  containerClassName={
@@ -43,14 +43,16 @@ const FormField = <
43
43
  const useFormField = () => {
44
44
  const fieldContext = React.useContext(FormFieldContext)
45
45
  const itemContext = React.useContext(FormItemContext)
46
- const { getFieldState } = useFormContext()
47
- const formState = useFormState({ name: fieldContext.name })
48
- const fieldState = getFieldState(fieldContext.name, formState)
49
46
 
50
- if (!fieldContext) {
47
+ // Check context before accessing its properties
48
+ if (!fieldContext?.name) {
51
49
  throw new Error("useFormField should be used within <FormField>")
52
50
  }
53
51
 
52
+ const { getFieldState } = useFormContext()
53
+ const formState = useFormState({ name: fieldContext.name })
54
+ const fieldState = getFieldState(fieldContext.name, formState)
55
+
54
56
  const { id } = itemContext
55
57
 
56
58
  return {
@@ -55,6 +55,11 @@ export function useAuthData<T>({
55
55
  const previousUserId = useRef<string | undefined>(undefined)
56
56
  const [error, setError] = useState<FetchError | null>(null)
57
57
 
58
+ // Use ref to access cacheEntry in refetch without creating dependency
59
+ // This prevents infinite loops from cacheEntry changes triggering refetch recreation
60
+ const cacheEntryRef = useRef(cacheEntry)
61
+ cacheEntryRef.current = cacheEntry
62
+
58
63
  const refetch = useCallback(async () => {
59
64
  // Check if there's already an in-flight request for this key
60
65
  const existingRequest = authDataCache.getInFlightRequest<{
@@ -76,8 +81,8 @@ export function useAuthData<T>({
76
81
  return
77
82
  }
78
83
 
79
- // Mark as refetching if we have cached data
80
- if (cacheEntry?.data !== undefined) {
84
+ // Mark as refetching if we have cached data (use ref to avoid dependency)
85
+ if (cacheEntryRef.current?.data !== undefined) {
81
86
  authDataCache.setRefetching(stableCacheKey, true)
82
87
  }
83
88
 
@@ -121,7 +126,8 @@ export function useAuthData<T>({
121
126
  authDataCache.setRefetching(stableCacheKey, false)
122
127
  authDataCache.removeInFlightRequest(stableCacheKey)
123
128
  }
124
- }, [stableCacheKey, toast, localization, localizeErrors, cacheEntry])
129
+ // Note: cacheEntry is accessed via ref to avoid infinite loop
130
+ }, [stableCacheKey, toast, localization, localizeErrors])
125
131
 
126
132
  useEffect(() => {
127
133
  const currentUserId = sessionData?.user?.id
@@ -34,6 +34,8 @@ export function useAuthenticate<TAuthClient extends AnyAuthClient>(
34
34
  | undefined
35
35
 
36
36
  useEffect(() => {
37
+ // SSR guard - window not available on server
38
+ if (typeof window === "undefined") return
37
39
  if (!enabled || isPending || sessionData) return
38
40
 
39
41
  const searchParams = new URLSearchParams(window.location.search)
@@ -60,28 +60,28 @@ export function useCaptcha({
60
60
  }
61
61
  case "google-recaptcha-v2-checkbox": {
62
62
  const recaptchaRef = captchaRef as RefObject<ReCAPTCHA>
63
- response = recaptchaRef.current.getValue()
63
+ response = recaptchaRef.current?.getValue?.()
64
64
  break
65
65
  }
66
66
  case "google-recaptcha-v2-invisible": {
67
67
  const recaptchaRef = captchaRef as RefObject<ReCAPTCHA>
68
- response = await recaptchaRef.current.executeAsync()
68
+ response = await recaptchaRef.current?.executeAsync?.()
69
69
  break
70
70
  }
71
71
  case "cloudflare-turnstile": {
72
72
  const turnstileRef = captchaRef as RefObject<TurnstileInstance>
73
- response = turnstileRef.current.getResponse()
73
+ response = turnstileRef.current?.getResponse?.()
74
74
  break
75
75
  }
76
76
  case "hcaptcha": {
77
77
  const hcaptchaRef = captchaRef as RefObject<HCaptcha>
78
- response = hcaptchaRef.current.getResponse()
78
+ response = hcaptchaRef.current?.getResponse?.()
79
79
  break
80
80
  }
81
81
  case "captchafox": {
82
82
  const captchafoxRef =
83
83
  captchaRef as RefObject<CaptchaFoxInstance>
84
- response = captchafoxRef.current.getResponse()
84
+ response = captchafoxRef.current?.getResponse?.()
85
85
  break
86
86
  }
87
87
  }
@@ -48,11 +48,17 @@ const DefaultLink: Link = ({ href, className, children }) => (
48
48
  )
49
49
 
50
50
  const defaultNavigate = (href: string) => {
51
- window.location.href = href
51
+ // SSR guard - window not available on server
52
+ if (typeof window !== "undefined") {
53
+ window.location.href = href
54
+ }
52
55
  }
53
56
 
54
57
  const defaultReplace = (href: string) => {
55
- window.location.replace(href)
58
+ // SSR guard - window not available on server
59
+ if (typeof window !== "undefined") {
60
+ window.location.replace(href)
61
+ }
56
62
  }
57
63
 
58
64
  // Default toast is a no-op - users should provide their own via onError/onSuccess
@@ -4,6 +4,13 @@ export async function resizeAndCropImage(
4
4
  size: number,
5
5
  extension: string
6
6
  ): Promise<File> {
7
+ // SSR guard - image processing requires browser APIs
8
+ if (typeof window === "undefined" || typeof document === "undefined") {
9
+ throw new Error(
10
+ "resizeAndCropImage requires a browser environment (window and document must be defined)"
11
+ )
12
+ }
13
+
7
14
  const image = await loadImage(file)
8
15
 
9
16
  const canvas = document.createElement("canvas")