@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,165 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react-native';
3
+ import { Avatar } from '../Avatar';
4
+
5
+ // Mock @sudobility/components-rn
6
+ jest.mock('@sudobility/components-rn', () => ({
7
+ cn: function () {
8
+ var args = Array.prototype.slice.call(arguments);
9
+ return args.filter(Boolean).join(' ');
10
+ },
11
+ }));
12
+
13
+ function createUser(overrides) {
14
+ return Object.assign(
15
+ {
16
+ uid: 'user-1',
17
+ email: 'john@example.com',
18
+ displayName: 'John Doe',
19
+ photoURL: null,
20
+ isAnonymous: false,
21
+ emailVerified: true,
22
+ providerId: 'password',
23
+ },
24
+ overrides
25
+ );
26
+ }
27
+
28
+ describe('Avatar', () => {
29
+ describe('initials display', () => {
30
+ it('shows two-letter initials from full display name', () => {
31
+ var user = createUser({ displayName: 'John Doe', photoURL: null });
32
+ render(<Avatar user={user} />);
33
+
34
+ expect(screen.getByText('JD')).toBeTruthy();
35
+ });
36
+
37
+ it('shows first two chars when display name is single word', () => {
38
+ var user = createUser({ displayName: 'Alice', photoURL: null });
39
+ render(<Avatar user={user} />);
40
+
41
+ expect(screen.getByText('AL')).toBeTruthy();
42
+ });
43
+
44
+ it('uses first and last name initials for multi-word names', () => {
45
+ var user = createUser({
46
+ displayName: 'Mary Jane Watson',
47
+ photoURL: null,
48
+ });
49
+ render(<Avatar user={user} />);
50
+
51
+ expect(screen.getByText('MW')).toBeTruthy();
52
+ });
53
+
54
+ it('falls back to email initials when no display name', () => {
55
+ var user = createUser({
56
+ displayName: null,
57
+ email: 'alice@example.com',
58
+ photoURL: null,
59
+ });
60
+ render(<Avatar user={user} />);
61
+
62
+ expect(screen.getByText('AL')).toBeTruthy();
63
+ });
64
+
65
+ it('shows "?" when no name or email', () => {
66
+ var user = createUser({
67
+ displayName: null,
68
+ email: null,
69
+ photoURL: null,
70
+ });
71
+ render(<Avatar user={user} />);
72
+
73
+ expect(screen.getByText('?')).toBeTruthy();
74
+ });
75
+ });
76
+
77
+ describe('photo display', () => {
78
+ it('renders an Image when photoURL is provided', () => {
79
+ var user = createUser({
80
+ photoURL: 'https://example.com/photo.jpg',
81
+ });
82
+ var result = render(<Avatar user={user} />);
83
+ var tree = JSON.stringify(result.toJSON());
84
+
85
+ expect(tree).toContain('https://example.com/photo.jpg');
86
+ });
87
+ });
88
+
89
+ describe('size prop', () => {
90
+ it('applies default size of 32', () => {
91
+ var user = createUser({ photoURL: null });
92
+ var result = render(<Avatar user={user} />);
93
+ var tree = JSON.stringify(result.toJSON());
94
+
95
+ expect(tree).toContain('"width":32');
96
+ expect(tree).toContain('"height":32');
97
+ });
98
+
99
+ it('applies custom size', () => {
100
+ var user = createUser({ photoURL: null });
101
+ var result = render(<Avatar user={user} size={64} />);
102
+ var tree = JSON.stringify(result.toJSON());
103
+
104
+ expect(tree).toContain('"width":64');
105
+ expect(tree).toContain('"height":64');
106
+ expect(tree).toContain('"borderRadius":32');
107
+ });
108
+
109
+ it('scales font size based on avatar size', () => {
110
+ var user = createUser({ photoURL: null });
111
+ var result = render(<Avatar user={user} size={100} />);
112
+ var tree = JSON.stringify(result.toJSON());
113
+
114
+ // fontSize should be size * 0.4 = 40
115
+ expect(tree).toContain('"fontSize":40');
116
+ });
117
+ });
118
+
119
+ describe('onPress', () => {
120
+ it('renders as Pressable when onPress is provided', () => {
121
+ var onPress = jest.fn();
122
+ var user = createUser({ photoURL: null });
123
+ render(<Avatar user={user} onPress={onPress} />);
124
+
125
+ var button = screen.getByRole('button');
126
+ expect(button).toBeTruthy();
127
+ });
128
+
129
+ it('calls onPress when pressed', () => {
130
+ var onPress = jest.fn();
131
+ var user = createUser({ photoURL: null });
132
+ render(<Avatar user={user} onPress={onPress} />);
133
+
134
+ fireEvent.press(screen.getByRole('button'));
135
+ expect(onPress).toHaveBeenCalledTimes(1);
136
+ });
137
+
138
+ it('renders as View (not Pressable) when onPress is not provided', () => {
139
+ var user = createUser({ photoURL: null });
140
+ render(<Avatar user={user} />);
141
+
142
+ expect(screen.queryByRole('button')).toBeNull();
143
+ });
144
+ });
145
+
146
+ describe('accessibility', () => {
147
+ it('uses display name for accessibility label on pressable', () => {
148
+ var onPress = jest.fn();
149
+ var user = createUser({ displayName: 'Jane Smith' });
150
+ render(<Avatar user={user} onPress={onPress} />);
151
+
152
+ var button = screen.getByRole('button');
153
+ expect(button.props.accessibilityLabel).toBe('Jane Smith');
154
+ });
155
+
156
+ it('falls back to "User avatar" when no display name', () => {
157
+ var onPress = jest.fn();
158
+ var user = createUser({ displayName: null });
159
+ render(<Avatar user={user} onPress={onPress} />);
160
+
161
+ var button = screen.getByRole('button');
162
+ expect(button.props.accessibilityLabel).toBe('User avatar');
163
+ });
164
+ });
165
+ });
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @sudobility/auth-components-rn
3
+ * React Native Authentication components with Firebase Auth support
4
+ */
5
+
6
+ // Context and hooks
7
+ export {
8
+ AuthProvider,
9
+ useAuthStatus,
10
+ createDefaultErrorTexts,
11
+ } from './AuthProvider';
12
+
13
+ // Components
14
+ export { AuthScreen } from './AuthScreen';
15
+ export { AuthInline } from './AuthInline';
16
+ export { AuthAction } from './AuthAction';
17
+ export { Avatar } from './Avatar';
18
+ export { ProviderButtons } from './ProviderButtons';
19
+ export { EmailSignInForm } from './EmailSignInForm';
20
+ export { EmailSignUpForm } from './EmailSignUpForm';
21
+ export { ForgotPasswordForm } from './ForgotPasswordForm';
22
+
23
+ // Types
24
+ export type {
25
+ // Provider types
26
+ AuthProviderType,
27
+ AuthProvidersConfig,
28
+ // User types
29
+ AuthUser,
30
+ // Text types (i18n)
31
+ AuthTexts,
32
+ AuthErrorTexts,
33
+ // Callbacks
34
+ AuthCallbacks,
35
+ // Context value
36
+ AuthContextValue,
37
+ // Component props
38
+ AuthProviderProps,
39
+ AuthMode,
40
+ AuthScreenProps,
41
+ AuthInlineProps,
42
+ AuthMenuItem,
43
+ AuthActionProps,
44
+ AvatarProps,
45
+ AuthContentProps,
46
+ EmailSignInFormProps,
47
+ EmailSignUpFormProps,
48
+ ForgotPasswordFormProps,
49
+ ProviderButtonsProps,
50
+ // Tracking
51
+ AuthTrackingData,
52
+ } from './types';
@@ -0,0 +1,30 @@
1
+ // Type declarations for NativeWind className prop
2
+ import 'react-native';
3
+
4
+ declare module 'react-native' {
5
+ interface ViewProps {
6
+ className?: string;
7
+ }
8
+ interface TextProps {
9
+ className?: string;
10
+ }
11
+ interface ImageProps {
12
+ className?: string;
13
+ }
14
+ interface PressableProps {
15
+ className?: string;
16
+ }
17
+ interface TouchableOpacityProps {
18
+ className?: string;
19
+ }
20
+ interface ScrollViewProps {
21
+ className?: string;
22
+ contentContainerClassName?: string;
23
+ }
24
+ interface TextInputProps {
25
+ className?: string;
26
+ }
27
+ interface SafeAreaViewProps {
28
+ className?: string;
29
+ }
30
+ }
package/src/types.ts ADDED
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Type definitions for auth-components-rn
3
+ * React Native authentication components with Firebase Auth support
4
+ */
5
+
6
+ import type { ReactNode } from 'react';
7
+
8
+ // ============ Auth Provider Types ============
9
+
10
+ /**
11
+ * Available authentication providers
12
+ */
13
+ export type AuthProviderType = 'google' | 'apple' | 'email';
14
+
15
+ /**
16
+ * Configuration for enabled auth providers
17
+ */
18
+ export interface AuthProvidersConfig {
19
+ /** Which providers to enable */
20
+ providers: AuthProviderType[];
21
+ /** Enable anonymous sign-in fallback */
22
+ enableAnonymous?: boolean;
23
+ }
24
+
25
+ // ============ User Types ============
26
+
27
+ /**
28
+ * User information exposed by the auth context
29
+ */
30
+ export interface AuthUser {
31
+ uid: string;
32
+ email: string | null;
33
+ displayName: string | null;
34
+ photoURL: string | null;
35
+ isAnonymous: boolean;
36
+ emailVerified: boolean;
37
+ providerId: string | null;
38
+ }
39
+
40
+ // ============ Text Types (i18n) ============
41
+
42
+ /**
43
+ * All text strings for auth components.
44
+ * Consumer provides these for full i18n support.
45
+ */
46
+ export interface AuthTexts {
47
+ // Titles
48
+ signInTitle: string;
49
+ signInWithEmail: string;
50
+ createAccount: string;
51
+ resetPassword: string;
52
+
53
+ // Buttons
54
+ signIn: string;
55
+ signUp: string;
56
+ logout: string;
57
+ login: string;
58
+ continueWithGoogle: string;
59
+ continueWithApple: string;
60
+ continueWithEmail: string;
61
+ sendResetLink: string;
62
+ backToSignIn: string;
63
+ close: string;
64
+
65
+ // Labels
66
+ email: string;
67
+ password: string;
68
+ confirmPassword: string;
69
+ displayName: string;
70
+
71
+ // Placeholders
72
+ emailPlaceholder: string;
73
+ passwordPlaceholder: string;
74
+ confirmPasswordPlaceholder: string;
75
+ displayNamePlaceholder: string;
76
+
77
+ // Links
78
+ forgotPassword: string;
79
+ noAccount: string;
80
+ haveAccount: string;
81
+ or: string;
82
+
83
+ // Messages
84
+ resetEmailSent: string;
85
+ resetEmailSentDesc: string;
86
+ passwordMismatch: string;
87
+ passwordTooShort: string;
88
+ loading: string;
89
+ }
90
+
91
+ /**
92
+ * Firebase error messages - parameterized for i18n
93
+ */
94
+ export interface AuthErrorTexts {
95
+ 'auth/user-not-found': string;
96
+ 'auth/wrong-password': string;
97
+ 'auth/invalid-email': string;
98
+ 'auth/invalid-credential': string;
99
+ 'auth/email-already-in-use': string;
100
+ 'auth/weak-password': string;
101
+ 'auth/too-many-requests': string;
102
+ 'auth/network-request-failed': string;
103
+ 'auth/popup-closed-by-user': string;
104
+ 'auth/popup-blocked': string;
105
+ 'auth/account-exists-with-different-credential': string;
106
+ 'auth/operation-not-allowed': string;
107
+ default: string;
108
+ }
109
+
110
+ // ============ Callbacks ============
111
+
112
+ /**
113
+ * Event callbacks for auth operations
114
+ */
115
+ export interface AuthCallbacks {
116
+ /** Called after successful sign in */
117
+ onSignIn?: (user: AuthUser) => void;
118
+ /** Called after sign out */
119
+ onSignOut?: () => void;
120
+ /** Called on auth error */
121
+ onError?: (error: Error, code?: string) => void;
122
+ }
123
+
124
+ // ============ Context Value ============
125
+
126
+ /**
127
+ * Value provided by AuthProvider context
128
+ */
129
+ export interface AuthContextValue {
130
+ user: AuthUser | null;
131
+ loading: boolean;
132
+ error: string | null;
133
+ isAuthenticated: boolean;
134
+ isAnonymous: boolean;
135
+
136
+ // Auth methods
137
+ signInWithGoogle: () => Promise<void>;
138
+ signInWithApple: () => Promise<void>;
139
+ signInWithEmail: (email: string, password: string) => Promise<void>;
140
+ signUpWithEmail: (
141
+ email: string,
142
+ password: string,
143
+ displayName?: string
144
+ ) => Promise<void>;
145
+ resetPassword: (email: string) => Promise<void>;
146
+ signOut: () => Promise<void>;
147
+ signInAnonymously: () => Promise<void>;
148
+
149
+ // Error management
150
+ clearError: () => void;
151
+
152
+ // Texts (for child components)
153
+ texts: AuthTexts;
154
+
155
+ // Provider config (for child components)
156
+ providerConfig: AuthProvidersConfig;
157
+ }
158
+
159
+ // ============ Component Props ============
160
+
161
+ /**
162
+ * Props for AuthProvider component
163
+ */
164
+ export interface AuthProviderProps {
165
+ children: ReactNode;
166
+ /** Provider configuration */
167
+ providerConfig: AuthProvidersConfig;
168
+ /** All text strings for i18n */
169
+ texts: AuthTexts;
170
+ /** Firebase error messages for i18n */
171
+ errorTexts: AuthErrorTexts;
172
+ /** Event callbacks */
173
+ callbacks?: AuthCallbacks;
174
+ /** Custom error message resolver (takes precedence over errorTexts) */
175
+ resolveErrorMessage?: (code: string) => string;
176
+ }
177
+
178
+ /**
179
+ * Auth UI mode
180
+ */
181
+ export type AuthMode =
182
+ | 'select'
183
+ | 'email-signin'
184
+ | 'email-signup'
185
+ | 'forgot-password';
186
+
187
+ /**
188
+ * Props for AuthScreen component (full-screen auth flow)
189
+ */
190
+ export interface AuthScreenProps {
191
+ /** Initial mode to show */
192
+ initialMode?: AuthMode;
193
+ /** Custom class name */
194
+ className?: string;
195
+ /** Which providers to show (defaults to providerConfig) */
196
+ providers?: AuthProviderType[];
197
+ /** Show title header (default: true) */
198
+ showTitle?: boolean;
199
+ /** Custom title (overrides texts) */
200
+ title?: string;
201
+ /** Callback when auth mode changes */
202
+ onModeChange?: (mode: AuthMode) => void;
203
+ /** Callback when auth succeeds */
204
+ onSuccess?: () => void;
205
+ /** Optional tracking callback */
206
+ onTrack?: (data: AuthTrackingData) => void;
207
+ /** Optional tracking label */
208
+ trackingLabel?: string;
209
+ /** Optional component name for tracking */
210
+ componentName?: string;
211
+ }
212
+
213
+ /**
214
+ * Props for AuthInline component (inline auth flow)
215
+ */
216
+ export interface AuthInlineProps {
217
+ /** Initial mode to show */
218
+ initialMode?: AuthMode;
219
+ /** Custom class name */
220
+ className?: string;
221
+ /** Which providers to show (defaults to providerConfig) */
222
+ providers?: AuthProviderType[];
223
+ /** Show title header (default: true) */
224
+ showTitle?: boolean;
225
+ /** Custom title (overrides texts) */
226
+ title?: string;
227
+ /** Callback when auth mode changes */
228
+ onModeChange?: (mode: AuthMode) => void;
229
+ /** Callback when auth succeeds */
230
+ onSuccess?: () => void;
231
+ /** Card style variant */
232
+ variant?: 'card' | 'flat' | 'bordered';
233
+ /** Optional tracking callback */
234
+ onTrack?: (data: AuthTrackingData) => void;
235
+ /** Optional tracking label */
236
+ trackingLabel?: string;
237
+ /** Optional component name for tracking */
238
+ componentName?: string;
239
+ }
240
+
241
+ /**
242
+ * Custom menu item for AuthAction dropdown
243
+ */
244
+ export interface AuthMenuItem {
245
+ /** Unique identifier */
246
+ id: string;
247
+ /** Display label */
248
+ label: string;
249
+ /** Press handler */
250
+ onPress: () => void;
251
+ /** Optional icon */
252
+ icon?: ReactNode;
253
+ /** Show divider after this item */
254
+ dividerAfter?: boolean;
255
+ /** Disabled state */
256
+ disabled?: boolean;
257
+ }
258
+
259
+ /**
260
+ * Props for AuthAction component (header component)
261
+ */
262
+ export interface AuthActionProps {
263
+ /** Custom class name */
264
+ className?: string;
265
+ /** Button variant when not logged in */
266
+ loginButtonVariant?: 'primary' | 'secondary' | 'outline' | 'ghost';
267
+ /** Button size */
268
+ size?: 'sm' | 'md' | 'lg';
269
+ /** Custom login button content */
270
+ loginButtonContent?: ReactNode;
271
+ /** Avatar size in pixels (default: 32) */
272
+ avatarSize?: number;
273
+ /** Custom menu items (rendered above logout) */
274
+ menuItems?: AuthMenuItem[];
275
+ /** Show user info section in menu (default: true) */
276
+ showUserInfo?: boolean;
277
+ /** Custom user info renderer */
278
+ renderUserInfo?: (user: AuthUser) => ReactNode;
279
+ /** Custom avatar renderer */
280
+ renderAvatar?: (user: AuthUser) => ReactNode;
281
+ /** Callback when login button pressed */
282
+ onLoginPress?: () => void | boolean;
283
+ /** Callback when logout pressed */
284
+ onLogoutPress?: () => void;
285
+ /** Optional tracking callback */
286
+ onTrack?: (data: AuthTrackingData) => void;
287
+ /** Optional tracking label */
288
+ trackingLabel?: string;
289
+ /** Optional component name for tracking */
290
+ componentName?: string;
291
+ }
292
+
293
+ /**
294
+ * Props for Avatar component
295
+ */
296
+ export interface AvatarProps {
297
+ /** User to display avatar for */
298
+ user: AuthUser;
299
+ /** Size in pixels (default: 32) */
300
+ size?: number;
301
+ /** Custom class name */
302
+ className?: string;
303
+ /** Press handler */
304
+ onPress?: () => void;
305
+ }
306
+
307
+ /**
308
+ * Props for internal AuthContent component
309
+ */
310
+ export interface AuthContentProps {
311
+ /** Current auth mode */
312
+ mode: AuthMode;
313
+ /** Mode change handler */
314
+ onModeChange: (mode: AuthMode) => void;
315
+ /** Override providers */
316
+ providers?: AuthProviderType[];
317
+ /** Success callback */
318
+ onSuccess?: () => void;
319
+ }
320
+
321
+ /**
322
+ * Props for form components
323
+ */
324
+ export interface EmailSignInFormProps {
325
+ onSwitchToSignUp: () => void;
326
+ onSwitchToForgotPassword: () => void;
327
+ onSuccess?: () => void;
328
+ /** Optional tracking callback */
329
+ onTrack?: (data: AuthTrackingData) => void;
330
+ /** Optional tracking label */
331
+ trackingLabel?: string;
332
+ /** Optional component name for tracking */
333
+ componentName?: string;
334
+ }
335
+
336
+ export interface EmailSignUpFormProps {
337
+ onSwitchToSignIn: () => void;
338
+ onSuccess?: () => void;
339
+ /** Optional tracking callback */
340
+ onTrack?: (data: AuthTrackingData) => void;
341
+ /** Optional tracking label */
342
+ trackingLabel?: string;
343
+ /** Optional component name for tracking */
344
+ componentName?: string;
345
+ }
346
+
347
+ export interface ForgotPasswordFormProps {
348
+ onSwitchToSignIn: () => void;
349
+ /** Optional tracking callback */
350
+ onTrack?: (data: AuthTrackingData) => void;
351
+ /** Optional tracking label */
352
+ trackingLabel?: string;
353
+ /** Optional component name for tracking */
354
+ componentName?: string;
355
+ }
356
+
357
+ /**
358
+ * Props for ProviderButtons component
359
+ */
360
+ export interface ProviderButtonsProps {
361
+ providers: AuthProviderType[];
362
+ onEmailPress: () => void;
363
+ /** Optional tracking callback */
364
+ onTrack?: (data: AuthTrackingData) => void;
365
+ /** Optional tracking label */
366
+ trackingLabel?: string;
367
+ /** Optional component name for tracking */
368
+ componentName?: string;
369
+ }
370
+
371
+ // ============ Tracking Types ============
372
+
373
+ /** Tracking data for auth component actions */
374
+ export interface AuthTrackingData {
375
+ action:
376
+ | 'login_press'
377
+ | 'logout_press'
378
+ | 'provider_press'
379
+ | 'form_submit'
380
+ | 'switch_mode';
381
+ trackingLabel?: string;
382
+ componentName?: string;
383
+ }