@sudobility/auth-components-rn 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/AuthAction.d.ts +7 -0
  2. package/dist/AuthAction.d.ts.map +1 -0
  3. package/dist/AuthInline.d.ts +7 -0
  4. package/dist/AuthInline.d.ts.map +1 -0
  5. package/dist/AuthProvider.d.ts +15 -0
  6. package/dist/AuthProvider.d.ts.map +1 -0
  7. package/dist/AuthScreen.d.ts +7 -0
  8. package/dist/AuthScreen.d.ts.map +1 -0
  9. package/dist/Avatar.d.ts +8 -0
  10. package/dist/Avatar.d.ts.map +1 -0
  11. package/dist/EmailSignInForm.d.ts +7 -0
  12. package/dist/EmailSignInForm.d.ts.map +1 -0
  13. package/dist/EmailSignUpForm.d.ts +7 -0
  14. package/dist/EmailSignUpForm.d.ts.map +1 -0
  15. package/dist/ForgotPasswordForm.d.ts +7 -0
  16. package/dist/ForgotPasswordForm.d.ts.map +1 -0
  17. package/dist/ProviderButtons.d.ts +7 -0
  18. package/dist/ProviderButtons.d.ts.map +1 -0
  19. package/dist/index.cjs.js +1850 -0
  20. package/dist/index.cjs.js.map +1 -0
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.esm.js +1850 -0
  24. package/dist/index.esm.js.map +1 -0
  25. package/dist/types.d.ts +313 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/package.json +64 -0
  28. package/src/AuthAction.tsx +107 -0
  29. package/src/AuthInline.tsx +124 -0
  30. package/src/AuthProvider.tsx +276 -0
  31. package/src/AuthScreen.tsx +117 -0
  32. package/src/Avatar.tsx +86 -0
  33. package/src/EmailSignInForm.tsx +130 -0
  34. package/src/EmailSignUpForm.tsx +161 -0
  35. package/src/ForgotPasswordForm.tsx +129 -0
  36. package/src/ProviderButtons.tsx +106 -0
  37. package/src/__tests__/AuthAction.test.tsx +261 -0
  38. package/src/__tests__/AuthProvider.test.tsx +459 -0
  39. package/src/__tests__/Avatar.test.tsx +165 -0
  40. package/src/index.ts +52 -0
  41. package/src/nativewind.d.ts +30 -0
  42. package/src/types.ts +383 -0
@@ -0,0 +1,261 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, act } from '@testing-library/react-native';
3
+ import { Text } from 'react-native';
4
+ import { AuthAction } from '../AuthAction';
5
+ import { AuthProvider, createDefaultErrorTexts } from '../AuthProvider';
6
+
7
+ // Mock @sudobility/components-rn
8
+ jest.mock('@sudobility/components-rn', () => ({
9
+ cn: function () {
10
+ var args = Array.prototype.slice.call(arguments);
11
+ return args.filter(Boolean).join(' ');
12
+ },
13
+ Button: function (props) {
14
+ var RN = require('react-native');
15
+ var R = require('react');
16
+ return R.createElement(
17
+ RN.Pressable,
18
+ { onPress: props.onPress, disabled: props.disabled, accessibilityRole: 'button' },
19
+ typeof props.children === 'string'
20
+ ? R.createElement(RN.Text, null, props.children)
21
+ : props.children
22
+ );
23
+ },
24
+ }));
25
+
26
+ var defaultTexts = {
27
+ signInTitle: 'Sign In',
28
+ signInWithEmail: 'Sign in with Email',
29
+ createAccount: 'Create Account',
30
+ resetPassword: 'Reset Password',
31
+ signIn: 'Sign In',
32
+ signUp: 'Sign Up',
33
+ logout: 'Log Out',
34
+ login: 'Log In',
35
+ continueWithGoogle: 'Continue with Google',
36
+ continueWithApple: 'Continue with Apple',
37
+ continueWithEmail: 'Continue with Email',
38
+ sendResetLink: 'Send Reset Link',
39
+ backToSignIn: 'Back to Sign In',
40
+ close: 'Close',
41
+ email: 'Email',
42
+ password: 'Password',
43
+ confirmPassword: 'Confirm Password',
44
+ displayName: 'Display Name',
45
+ emailPlaceholder: 'you@example.com',
46
+ passwordPlaceholder: 'Enter password',
47
+ confirmPasswordPlaceholder: 'Confirm password',
48
+ displayNamePlaceholder: 'Your name',
49
+ forgotPassword: 'Forgot password?',
50
+ noAccount: "Don't have an account?",
51
+ haveAccount: 'Already have an account?',
52
+ or: 'or',
53
+ resetEmailSent: 'Email Sent',
54
+ resetEmailSentDesc: 'Check {{email}} for a reset link.',
55
+ passwordMismatch: 'Passwords do not match',
56
+ passwordTooShort: 'Password must be at least 6 characters',
57
+ loading: 'Loading...',
58
+ };
59
+
60
+ var defaultErrorTexts = createDefaultErrorTexts();
61
+
62
+ var defaultProviderConfig = {
63
+ providers: ['google', 'email'],
64
+ };
65
+
66
+ function renderWithProvider(ui) {
67
+ return render(
68
+ <AuthProvider
69
+ providerConfig={defaultProviderConfig}
70
+ texts={defaultTexts}
71
+ errorTexts={defaultErrorTexts}
72
+ >
73
+ {ui}
74
+ </AuthProvider>
75
+ );
76
+ }
77
+
78
+ var mockUser = {
79
+ uid: 'user-123',
80
+ email: 'test@example.com',
81
+ displayName: 'Test User',
82
+ photoURL: null,
83
+ isAnonymous: false,
84
+ emailVerified: true,
85
+ providerId: 'password',
86
+ };
87
+
88
+ describe('AuthAction', () => {
89
+ describe('unauthenticated state', () => {
90
+ it('shows login button when not authenticated', async () => {
91
+ renderWithProvider(<AuthAction />);
92
+
93
+ await act(async () => {});
94
+
95
+ expect(screen.getByText('Log In')).toBeTruthy();
96
+ });
97
+
98
+ it('shows custom login button content', async () => {
99
+ renderWithProvider(
100
+ <AuthAction loginButtonContent="Get Started" />
101
+ );
102
+
103
+ await act(async () => {});
104
+
105
+ expect(screen.getByText('Get Started')).toBeTruthy();
106
+ });
107
+
108
+ it('calls onLoginPress when login button is pressed', async () => {
109
+ var onLoginPress = jest.fn();
110
+ renderWithProvider(<AuthAction onLoginPress={onLoginPress} />);
111
+
112
+ await act(async () => {});
113
+
114
+ fireEvent.press(screen.getByText('Log In'));
115
+ expect(onLoginPress).toHaveBeenCalledTimes(1);
116
+ });
117
+
118
+ it('fires tracking event on login press', async () => {
119
+ var onTrack = jest.fn();
120
+ renderWithProvider(
121
+ <AuthAction onTrack={onTrack} trackingLabel="header" />
122
+ );
123
+
124
+ await act(async () => {});
125
+
126
+ fireEvent.press(screen.getByText('Log In'));
127
+ expect(onTrack).toHaveBeenCalledWith({
128
+ action: 'login_press',
129
+ trackingLabel: 'header',
130
+ componentName: 'AuthAction',
131
+ });
132
+ });
133
+ });
134
+
135
+ describe('authenticated state', () => {
136
+ var mockSignOut;
137
+
138
+ beforeEach(() => {
139
+ mockSignOut = jest.fn().mockResolvedValue(undefined);
140
+
141
+ jest.spyOn(
142
+ require('../AuthProvider'),
143
+ 'useAuthStatus'
144
+ ).mockReturnValue({
145
+ user: mockUser,
146
+ loading: false,
147
+ error: null,
148
+ isAuthenticated: true,
149
+ isAnonymous: false,
150
+ signInWithGoogle: jest.fn(),
151
+ signInWithApple: jest.fn(),
152
+ signInWithEmail: jest.fn(),
153
+ signUpWithEmail: jest.fn(),
154
+ resetPassword: jest.fn(),
155
+ signOut: mockSignOut,
156
+ signInAnonymously: jest.fn(),
157
+ clearError: jest.fn(),
158
+ texts: defaultTexts,
159
+ providerConfig: defaultProviderConfig,
160
+ });
161
+ });
162
+
163
+ afterEach(() => {
164
+ jest.restoreAllMocks();
165
+ });
166
+
167
+ it('shows user display name when authenticated', () => {
168
+ renderWithProvider(<AuthAction />);
169
+
170
+ expect(screen.getByText('Test User')).toBeTruthy();
171
+ });
172
+
173
+ it('shows user email when displayName and email both exist', () => {
174
+ renderWithProvider(<AuthAction />);
175
+
176
+ expect(screen.getByText('test@example.com')).toBeTruthy();
177
+ });
178
+
179
+ it('shows logout button when authenticated', () => {
180
+ renderWithProvider(<AuthAction />);
181
+
182
+ expect(screen.getByText('Log Out')).toBeTruthy();
183
+ });
184
+
185
+ it('calls signOut and onLogoutPress when logout is pressed', async () => {
186
+ var onLogoutPress = jest.fn();
187
+ renderWithProvider(<AuthAction onLogoutPress={onLogoutPress} />);
188
+
189
+ await act(async () => {
190
+ fireEvent.press(screen.getByText('Log Out'));
191
+ });
192
+
193
+ expect(onLogoutPress).toHaveBeenCalledTimes(1);
194
+ expect(mockSignOut).toHaveBeenCalledTimes(1);
195
+ });
196
+
197
+ it('fires tracking event on logout press', async () => {
198
+ var onTrack = jest.fn();
199
+ renderWithProvider(
200
+ <AuthAction onTrack={onTrack} trackingLabel="header" />
201
+ );
202
+
203
+ await act(async () => {
204
+ fireEvent.press(screen.getByText('Log Out'));
205
+ });
206
+
207
+ expect(onTrack).toHaveBeenCalledWith({
208
+ action: 'logout_press',
209
+ trackingLabel: 'header',
210
+ componentName: 'AuthAction',
211
+ });
212
+ });
213
+
214
+ it('hides user info when showUserInfo is false', () => {
215
+ renderWithProvider(<AuthAction showUserInfo={false} />);
216
+
217
+ expect(screen.queryByText('Test User')).toBeNull();
218
+ expect(screen.queryByText('test@example.com')).toBeNull();
219
+ });
220
+
221
+ it('shows email as primary text when displayName is null', () => {
222
+ jest.spyOn(
223
+ require('../AuthProvider'),
224
+ 'useAuthStatus'
225
+ ).mockReturnValue({
226
+ user: Object.assign({}, mockUser, { displayName: null }),
227
+ loading: false,
228
+ error: null,
229
+ isAuthenticated: true,
230
+ isAnonymous: false,
231
+ signInWithGoogle: jest.fn(),
232
+ signInWithApple: jest.fn(),
233
+ signInWithEmail: jest.fn(),
234
+ signUpWithEmail: jest.fn(),
235
+ resetPassword: jest.fn(),
236
+ signOut: mockSignOut,
237
+ signInAnonymously: jest.fn(),
238
+ clearError: jest.fn(),
239
+ texts: defaultTexts,
240
+ providerConfig: defaultProviderConfig,
241
+ });
242
+
243
+ renderWithProvider(<AuthAction />);
244
+
245
+ expect(screen.getByText('test@example.com')).toBeTruthy();
246
+ });
247
+
248
+ it('uses custom renderAvatar when provided', () => {
249
+ renderWithProvider(
250
+ <AuthAction
251
+ renderAvatar={function (user) {
252
+ return <Text testID="custom-avatar">{user.uid}</Text>;
253
+ }}
254
+ />
255
+ );
256
+
257
+ expect(screen.getByTestId('custom-avatar')).toBeTruthy();
258
+ expect(screen.getByText('user-123')).toBeTruthy();
259
+ });
260
+ });
261
+ });
@@ -0,0 +1,459 @@
1
+ import React from 'react';
2
+ import { render, screen, act } from '@testing-library/react-native';
3
+ import { Text } from 'react-native';
4
+ import {
5
+ AuthProvider,
6
+ useAuthStatus,
7
+ createDefaultErrorTexts,
8
+ } from '../AuthProvider';
9
+
10
+ // Mock @sudobility/components-rn (imported by child components)
11
+ jest.mock('@sudobility/components-rn', () => ({
12
+ cn: function () {
13
+ var args = Array.prototype.slice.call(arguments);
14
+ return args.filter(Boolean).join(' ');
15
+ },
16
+ Button: function (props) {
17
+ var RN = require('react-native');
18
+ var R = require('react');
19
+ return R.createElement(
20
+ RN.Pressable,
21
+ { onPress: props.onPress, disabled: props.disabled, accessibilityRole: 'button' },
22
+ typeof props.children === 'string'
23
+ ? R.createElement(RN.Text, null, props.children)
24
+ : props.children
25
+ );
26
+ },
27
+ Card: function (props) {
28
+ var RN = require('react-native');
29
+ var R = require('react');
30
+ return R.createElement(RN.View, null, props.children);
31
+ },
32
+ }));
33
+
34
+ var defaultTexts = {
35
+ signInTitle: 'Sign In',
36
+ signInWithEmail: 'Sign in with Email',
37
+ createAccount: 'Create Account',
38
+ resetPassword: 'Reset Password',
39
+ signIn: 'Sign In',
40
+ signUp: 'Sign Up',
41
+ logout: 'Log Out',
42
+ login: 'Log In',
43
+ continueWithGoogle: 'Continue with Google',
44
+ continueWithApple: 'Continue with Apple',
45
+ continueWithEmail: 'Continue with Email',
46
+ sendResetLink: 'Send Reset Link',
47
+ backToSignIn: 'Back to Sign In',
48
+ close: 'Close',
49
+ email: 'Email',
50
+ password: 'Password',
51
+ confirmPassword: 'Confirm Password',
52
+ displayName: 'Display Name',
53
+ emailPlaceholder: 'you@example.com',
54
+ passwordPlaceholder: 'Enter password',
55
+ confirmPasswordPlaceholder: 'Confirm password',
56
+ displayNamePlaceholder: 'Your name',
57
+ forgotPassword: 'Forgot password?',
58
+ noAccount: "Don't have an account?",
59
+ haveAccount: 'Already have an account?',
60
+ or: 'or',
61
+ resetEmailSent: 'Email Sent',
62
+ resetEmailSentDesc: 'Check {{email}} for a reset link.',
63
+ passwordMismatch: 'Passwords do not match',
64
+ passwordTooShort: 'Password must be at least 6 characters',
65
+ loading: 'Loading...',
66
+ };
67
+
68
+ var defaultErrorTexts = createDefaultErrorTexts();
69
+
70
+ var defaultProviderConfig = {
71
+ providers: ['google', 'email'],
72
+ };
73
+
74
+ function renderWithProvider(ui, options) {
75
+ var opts = options || {};
76
+ return render(
77
+ <AuthProvider
78
+ providerConfig={defaultProviderConfig}
79
+ texts={defaultTexts}
80
+ errorTexts={defaultErrorTexts}
81
+ callbacks={opts.callbacks}
82
+ resolveErrorMessage={opts.resolveErrorMessage}
83
+ >
84
+ {ui}
85
+ </AuthProvider>
86
+ );
87
+ }
88
+
89
+ /** Helper component that exposes context values via text */
90
+ function AuthStatusDisplay() {
91
+ var ctx = useAuthStatus();
92
+ return (
93
+ <>
94
+ <Text testID="loading">{String(ctx.loading)}</Text>
95
+ <Text testID="error">{ctx.error != null ? ctx.error : 'null'}</Text>
96
+ <Text testID="isAuthenticated">{String(ctx.isAuthenticated)}</Text>
97
+ <Text testID="isAnonymous">{String(ctx.isAnonymous)}</Text>
98
+ <Text testID="user">{ctx.user ? ctx.user.uid : 'null'}</Text>
99
+ </>
100
+ );
101
+ }
102
+
103
+ describe('AuthProvider', () => {
104
+ describe('createDefaultErrorTexts', () => {
105
+ it('returns an object with expected error keys', () => {
106
+ var texts = createDefaultErrorTexts();
107
+ expect(texts['auth/user-not-found']).toBe(
108
+ 'No account found with this email.'
109
+ );
110
+ expect(texts['auth/wrong-password']).toBe('Incorrect password.');
111
+ expect(texts.default).toBe('An error occurred. Please try again.');
112
+ });
113
+ });
114
+
115
+ describe('useAuthStatus outside provider', () => {
116
+ it('throws when used outside AuthProvider', () => {
117
+ var spy = jest.spyOn(console, 'error').mockImplementation(function () {});
118
+
119
+ expect(function () {
120
+ render(<AuthStatusDisplay />);
121
+ }).toThrow('useAuthStatus must be used within an AuthProvider');
122
+
123
+ spy.mockRestore();
124
+ });
125
+ });
126
+
127
+ describe('initial state', () => {
128
+ it('starts with loading false after mount and no user', async () => {
129
+ renderWithProvider(<AuthStatusDisplay />);
130
+
131
+ await act(async () => {});
132
+
133
+ expect(screen.getByTestId('loading').props.children).toBe('false');
134
+ expect(screen.getByTestId('user').props.children).toBe('null');
135
+ expect(screen.getByTestId('isAuthenticated').props.children).toBe('false');
136
+ expect(screen.getByTestId('isAnonymous').props.children).toBe('false');
137
+ expect(screen.getByTestId('error').props.children).toBe('null');
138
+ });
139
+ });
140
+
141
+ describe('signOut', () => {
142
+ it('calls onSignOut callback on sign out', async () => {
143
+ var onSignOut = jest.fn();
144
+
145
+ function SignOutTrigger() {
146
+ var ctx = useAuthStatus();
147
+ return (
148
+ <Text testID="signout" onPress={function () { ctx.signOut(); }}>
149
+ Sign Out
150
+ </Text>
151
+ );
152
+ }
153
+
154
+ renderWithProvider(
155
+ <>
156
+ <AuthStatusDisplay />
157
+ <SignOutTrigger />
158
+ </>,
159
+ { callbacks: { onSignOut: onSignOut } }
160
+ );
161
+
162
+ await act(async () => {});
163
+
164
+ await act(async () => {
165
+ screen.getByTestId('signout').props.onPress();
166
+ });
167
+
168
+ expect(onSignOut).toHaveBeenCalledTimes(1);
169
+ });
170
+ });
171
+
172
+ describe('signInWithEmail', () => {
173
+ it('sets error when sign-in fails (placeholder implementation)', async () => {
174
+ var onError = jest.fn();
175
+
176
+ function SignInTrigger() {
177
+ var ctx = useAuthStatus();
178
+ return (
179
+ <Text
180
+ testID="signin"
181
+ onPress={function () { ctx.signInWithEmail('test@test.com', 'pass'); }}
182
+ >
183
+ Sign In
184
+ </Text>
185
+ );
186
+ }
187
+
188
+ renderWithProvider(
189
+ <>
190
+ <AuthStatusDisplay />
191
+ <SignInTrigger />
192
+ </>,
193
+ { callbacks: { onError: onError } }
194
+ );
195
+
196
+ await act(async () => {});
197
+
198
+ await act(async () => {
199
+ screen.getByTestId('signin').props.onPress();
200
+ });
201
+
202
+ expect(screen.getByTestId('error').props.children).toBe(
203
+ 'An error occurred. Please try again.'
204
+ );
205
+ expect(onError).toHaveBeenCalledTimes(1);
206
+ });
207
+ });
208
+
209
+ describe('signUpWithEmail', () => {
210
+ it('sets error when sign-up fails (placeholder implementation)', async () => {
211
+ function SignUpTrigger() {
212
+ var ctx = useAuthStatus();
213
+ return (
214
+ <Text
215
+ testID="signup"
216
+ onPress={function () {
217
+ ctx.signUpWithEmail('test@test.com', 'pass123', 'Test User');
218
+ }}
219
+ >
220
+ Sign Up
221
+ </Text>
222
+ );
223
+ }
224
+
225
+ renderWithProvider(
226
+ <>
227
+ <AuthStatusDisplay />
228
+ <SignUpTrigger />
229
+ </>
230
+ );
231
+
232
+ await act(async () => {});
233
+
234
+ await act(async () => {
235
+ screen.getByTestId('signup').props.onPress();
236
+ });
237
+
238
+ expect(screen.getByTestId('error').props.children).not.toBe('null');
239
+ });
240
+ });
241
+
242
+ describe('signInWithGoogle', () => {
243
+ it('sets error when Google sign-in fails (placeholder)', async () => {
244
+ function GoogleTrigger() {
245
+ var ctx = useAuthStatus();
246
+ return (
247
+ <Text testID="google" onPress={function () { ctx.signInWithGoogle(); }}>
248
+ Google
249
+ </Text>
250
+ );
251
+ }
252
+
253
+ renderWithProvider(
254
+ <>
255
+ <AuthStatusDisplay />
256
+ <GoogleTrigger />
257
+ </>
258
+ );
259
+
260
+ await act(async () => {});
261
+
262
+ await act(async () => {
263
+ screen.getByTestId('google').props.onPress();
264
+ });
265
+
266
+ expect(screen.getByTestId('error').props.children).not.toBe('null');
267
+ });
268
+ });
269
+
270
+ describe('signInWithApple', () => {
271
+ it('sets error when Apple sign-in fails (placeholder)', async () => {
272
+ function AppleTrigger() {
273
+ var ctx = useAuthStatus();
274
+ return (
275
+ <Text testID="apple" onPress={function () { ctx.signInWithApple(); }}>
276
+ Apple
277
+ </Text>
278
+ );
279
+ }
280
+
281
+ renderWithProvider(
282
+ <>
283
+ <AuthStatusDisplay />
284
+ <AppleTrigger />
285
+ </>
286
+ );
287
+
288
+ await act(async () => {});
289
+
290
+ await act(async () => {
291
+ screen.getByTestId('apple').props.onPress();
292
+ });
293
+
294
+ expect(screen.getByTestId('error').props.children).not.toBe('null');
295
+ });
296
+ });
297
+
298
+ describe('resetPassword', () => {
299
+ it('sets error when password reset fails (placeholder)', async () => {
300
+ function ResetTrigger() {
301
+ var ctx = useAuthStatus();
302
+ return (
303
+ <Text testID="reset" onPress={function () { ctx.resetPassword('test@test.com'); }}>
304
+ Reset
305
+ </Text>
306
+ );
307
+ }
308
+
309
+ renderWithProvider(
310
+ <>
311
+ <AuthStatusDisplay />
312
+ <ResetTrigger />
313
+ </>
314
+ );
315
+
316
+ await act(async () => {});
317
+
318
+ await act(async () => {
319
+ screen.getByTestId('reset').props.onPress();
320
+ });
321
+
322
+ expect(screen.getByTestId('error').props.children).not.toBe('null');
323
+ });
324
+ });
325
+
326
+ describe('signInAnonymously', () => {
327
+ it('sets error when anonymous sign-in fails (placeholder)', async () => {
328
+ function AnonTrigger() {
329
+ var ctx = useAuthStatus();
330
+ return (
331
+ <Text testID="anon" onPress={function () { ctx.signInAnonymously(); }}>
332
+ Anon
333
+ </Text>
334
+ );
335
+ }
336
+
337
+ renderWithProvider(
338
+ <>
339
+ <AuthStatusDisplay />
340
+ <AnonTrigger />
341
+ </>
342
+ );
343
+
344
+ await act(async () => {});
345
+
346
+ await act(async () => {
347
+ screen.getByTestId('anon').props.onPress();
348
+ });
349
+
350
+ expect(screen.getByTestId('error').props.children).not.toBe('null');
351
+ });
352
+ });
353
+
354
+ describe('clearError', () => {
355
+ it('clears error state', async () => {
356
+ function ErrorTrigger() {
357
+ var ctx = useAuthStatus();
358
+ return (
359
+ <>
360
+ <Text
361
+ testID="signin"
362
+ onPress={function () { ctx.signInWithEmail('a@b.com', 'p'); }}
363
+ >
364
+ Sign In
365
+ </Text>
366
+ <Text testID="clear" onPress={function () { ctx.clearError(); }}>
367
+ Clear
368
+ </Text>
369
+ </>
370
+ );
371
+ }
372
+
373
+ renderWithProvider(
374
+ <>
375
+ <AuthStatusDisplay />
376
+ <ErrorTrigger />
377
+ </>
378
+ );
379
+
380
+ await act(async () => {});
381
+
382
+ // Trigger an error
383
+ await act(async () => {
384
+ screen.getByTestId('signin').props.onPress();
385
+ });
386
+
387
+ expect(screen.getByTestId('error').props.children).not.toBe('null');
388
+
389
+ // Clear the error
390
+ await act(async () => {
391
+ screen.getByTestId('clear').props.onPress();
392
+ });
393
+
394
+ expect(screen.getByTestId('error').props.children).toBe('null');
395
+ });
396
+ });
397
+
398
+ describe('resolveErrorMessage', () => {
399
+ it('uses custom resolveErrorMessage when provided', async () => {
400
+ var customResolver = jest.fn(function (code) {
401
+ return 'Custom: ' + code;
402
+ });
403
+
404
+ function ErrorTrigger() {
405
+ var ctx = useAuthStatus();
406
+ return (
407
+ <Text
408
+ testID="signin"
409
+ onPress={function () { ctx.signInWithEmail('a@b.com', 'p'); }}
410
+ >
411
+ Sign In
412
+ </Text>
413
+ );
414
+ }
415
+
416
+ renderWithProvider(
417
+ <>
418
+ <AuthStatusDisplay />
419
+ <ErrorTrigger />
420
+ </>,
421
+ { resolveErrorMessage: customResolver }
422
+ );
423
+
424
+ await act(async () => {});
425
+
426
+ await act(async () => {
427
+ screen.getByTestId('signin').props.onPress();
428
+ });
429
+
430
+ expect(customResolver).toHaveBeenCalled();
431
+ expect(screen.getByTestId('error').props.children).toMatch(/^Custom:/);
432
+ });
433
+ });
434
+
435
+ describe('context values', () => {
436
+ it('exposes texts and providerConfig from provider', async () => {
437
+ function ConfigDisplay() {
438
+ var ctx = useAuthStatus();
439
+ return (
440
+ <>
441
+ <Text testID="login-text">{ctx.texts.login}</Text>
442
+ <Text testID="providers">
443
+ {ctx.providerConfig.providers.join(',')}
444
+ </Text>
445
+ </>
446
+ );
447
+ }
448
+
449
+ renderWithProvider(<ConfigDisplay />);
450
+
451
+ await act(async () => {});
452
+
453
+ expect(screen.getByTestId('login-text').props.children).toBe('Log In');
454
+ expect(screen.getByTestId('providers').props.children).toBe(
455
+ 'google,email'
456
+ );
457
+ });
458
+ });
459
+ });