@umituz/react-native-auth 3.6.72 → 3.6.74

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/domain/value-objects/AuthConfig.ts +9 -56
  3. package/src/index.ts +15 -111
  4. package/src/infrastructure/adapters/StorageProviderAdapter.ts +1 -4
  5. package/src/infrastructure/providers/FirebaseAuthProvider.ts +5 -32
  6. package/src/infrastructure/repositories/AuthRepository.ts +0 -5
  7. package/src/infrastructure/services/AnonymousModeService.ts +6 -20
  8. package/src/infrastructure/services/AuthEventService.ts +2 -4
  9. package/src/infrastructure/services/initializeAuth.ts +2 -11
  10. package/src/infrastructure/utils/AuthErrorMapper.ts +0 -4
  11. package/src/infrastructure/utils/authStateHandler.ts +2 -4
  12. package/src/infrastructure/utils/listener/anonymousSignInHandler.ts +2 -22
  13. package/src/infrastructure/utils/listener/listenerLifecycle.util.ts +144 -0
  14. package/src/infrastructure/utils/listener/listenerState.util.ts +125 -0
  15. package/src/init/createAuthInitModule.ts +3 -22
  16. package/src/presentation/components/AccountActions.tsx +11 -43
  17. package/src/presentation/components/AuthBottomSheet.tsx +1 -17
  18. package/src/presentation/components/ProfileSection.tsx +78 -108
  19. package/src/presentation/components/RegisterForm.tsx +6 -28
  20. package/src/presentation/hooks/useAccountManagement.ts +0 -7
  21. package/src/presentation/hooks/useAuth.ts +2 -4
  22. package/src/presentation/hooks/useAuthBottomSheet.ts +23 -79
  23. package/src/presentation/hooks/useGoogleAuth.ts +0 -3
  24. package/src/presentation/hooks/useLoginForm.ts +26 -30
  25. package/src/presentation/hooks/useProfileEdit.ts +56 -64
  26. package/src/presentation/hooks/useRegisterForm.ts +41 -44
  27. package/src/presentation/providers/AuthProvider.tsx +2 -7
  28. package/src/presentation/stores/authModalStore.ts +0 -7
  29. package/src/presentation/stores/authStore.ts +1 -28
  30. package/src/presentation/stores/initializeAuthListener.ts +30 -160
  31. package/src/presentation/utils/accountDeleteHandler.util.ts +0 -51
  32. package/src/presentation/utils/authTransition.util.ts +72 -0
  33. package/src/presentation/utils/form/formFieldState.util.ts +82 -0
  34. package/src/presentation/utils/form/formValidation.util.ts +173 -0
  35. package/src/presentation/utils/socialAuthHandler.util.ts +106 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Auth Listener Lifecycle Utilities
3
+ * Handles subscription and cleanup logic for auth listener
4
+ */
5
+
6
+ import type { Auth, User } from "firebase/auth";
7
+ import type { AuthActions } from "../../../types/auth-store.types";
8
+ import { createAnonymousSignInHandler } from "./anonymousSignInHandler";
9
+ import {
10
+ isListenerInitialized,
11
+ completeInitialization,
12
+ setUnsubscribe,
13
+ incrementRefCount,
14
+ decrementRefCount,
15
+ startAnonymousSignIn,
16
+ completeAnonymousSignIn,
17
+ resetListenerState,
18
+ } from "./listenerState.util";
19
+ import { onIdTokenChanged } from "firebase/auth";
20
+ import { getAuthService } from "../../../infrastructure/services/AuthService";
21
+
22
+ type Store = AuthActions & { isAnonymous: boolean };
23
+
24
+ /**
25
+ * Create unsubscribe function that decrements ref count
26
+ */
27
+ export function createUnsubscribeHandler(): () => void {
28
+ return () => {
29
+ const { shouldCleanup } = decrementRefCount();
30
+
31
+ if (shouldCleanup) {
32
+ resetListenerState();
33
+ }
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Return existing unsubscribe if already initialized
39
+ * Returns null if initialization is in progress
40
+ */
41
+ export function handleExistingInitialization(): (() => void) | null {
42
+ if (!isListenerInitialized()) {
43
+ return null;
44
+ }
45
+
46
+ incrementRefCount();
47
+ return createUnsubscribeHandler();
48
+ }
49
+
50
+ /**
51
+ * Return no-op if initialization is in progress
52
+ */
53
+ export function handleInitializationInProgress(): () => void {
54
+ return () => {
55
+ // No-op - handled by initial initialization
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Setup Firebase auth listener
61
+ */
62
+ export function setupAuthListener(
63
+ auth: Auth,
64
+ store: Store,
65
+ autoAnonymousSignIn: boolean,
66
+ onAuthStateChange?: (user: User | null) => void
67
+ ): void {
68
+ const service = getAuthService();
69
+
70
+ if (service) {
71
+ const isAnonymous = service.getIsAnonymousMode();
72
+ if (isAnonymous) {
73
+ store.setIsAnonymous(true);
74
+ }
75
+ }
76
+
77
+ const unsubscribe = onIdTokenChanged(auth, (user) => {
78
+ handleAuthStateChange(user, store, auth, autoAnonymousSignIn, onAuthStateChange);
79
+ });
80
+
81
+ setUnsubscribe(unsubscribe);
82
+ }
83
+
84
+ /**
85
+ * Handle auth state change from Firebase
86
+ */
87
+ function handleAuthStateChange(
88
+ user: User | null,
89
+ store: Store,
90
+ auth: Auth,
91
+ autoAnonymousSignIn: boolean,
92
+ onAuthStateChange?: (user: User | null) => void
93
+ ): void {
94
+ if (!user && autoAnonymousSignIn) {
95
+ handleAnonymousMode(store, auth);
96
+ store.setFirebaseUser(null);
97
+ completeInitialization();
98
+ return;
99
+ }
100
+
101
+ store.setFirebaseUser(user);
102
+ store.setInitialized(true);
103
+
104
+ // Handle conversion from anonymous
105
+ if (user && !user.isAnonymous && store.isAnonymous) {
106
+ store.setIsAnonymous(false);
107
+ }
108
+
109
+ onAuthStateChange?.(user);
110
+ }
111
+
112
+ /**
113
+ * Handle anonymous mode sign-in
114
+ */
115
+ function handleAnonymousMode(store: Store, auth: Auth): void {
116
+ if (!startAnonymousSignIn()) {
117
+ return; // Already signing in
118
+ }
119
+
120
+ const handleAnonymousSignIn = createAnonymousSignInHandler(auth, store);
121
+
122
+ // Start sign-in without blocking
123
+ void (async () => {
124
+ try {
125
+ await handleAnonymousSignIn();
126
+ } finally {
127
+ completeAnonymousSignIn();
128
+ }
129
+ })();
130
+ }
131
+
132
+ /**
133
+ * Handle case where Firebase auth is not available
134
+ */
135
+ export function handleNoFirebaseAuth(store: Store): () => void {
136
+ completeInitialization();
137
+ store.setLoading(false);
138
+ store.setInitialized(true);
139
+ return () => {};
140
+ }
141
+
142
+ export function completeListenerSetup() {
143
+ completeInitialization();
144
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Auth Listener State Management
3
+ * Manages the state of Firebase auth listener initialization and lifecycle
4
+ */
5
+
6
+ export interface ListenerState {
7
+ initialized: boolean;
8
+ refCount: number;
9
+ initializationInProgress: boolean;
10
+ anonymousSignInInProgress: boolean;
11
+ unsubscribe: (() => void) | null;
12
+ }
13
+
14
+ const state: ListenerState = {
15
+ initialized: false,
16
+ refCount: 0,
17
+ initializationInProgress: false,
18
+ anonymousSignInInProgress: false,
19
+ unsubscribe: null,
20
+ };
21
+
22
+ /**
23
+ * Check if listener is initialized
24
+ */
25
+ export function isListenerInitialized(): boolean {
26
+ return state.initialized;
27
+ }
28
+
29
+ /**
30
+ * Check if initialization is in progress
31
+ */
32
+ export function isInitializationInProgress(): boolean {
33
+ return state.initializationInProgress;
34
+ }
35
+
36
+ /**
37
+ * Check if anonymous sign-in is in progress
38
+ */
39
+ export function isAnonymousSignInInProgress(): boolean {
40
+ return state.anonymousSignInInProgress;
41
+ }
42
+
43
+ /**
44
+ * Get current reference count
45
+ */
46
+ export function getRefCount(): number {
47
+ return state.refCount;
48
+ }
49
+
50
+ /**
51
+ * Mark initialization as started
52
+ */
53
+ export function startInitialization(): boolean {
54
+ if (state.initializationInProgress) {
55
+ return false; // Already initializing
56
+ }
57
+ state.initializationInProgress = true;
58
+ return true;
59
+ }
60
+
61
+ /**
62
+ * Complete initialization
63
+ */
64
+ export function completeInitialization(): void {
65
+ state.initializationInProgress = false;
66
+ state.initialized = true;
67
+ state.refCount = 1;
68
+ }
69
+
70
+ /**
71
+ * Set the unsubscribe function
72
+ */
73
+ export function setUnsubscribe(fn: (() => void) | null): void {
74
+ state.unsubscribe = fn;
75
+ }
76
+
77
+ /**
78
+ * Increment reference count when a new subscriber joins
79
+ */
80
+ export function incrementRefCount(): number {
81
+ state.refCount++;
82
+ return state.refCount;
83
+ }
84
+
85
+ /**
86
+ * Decrement reference count when a subscriber leaves
87
+ * Returns true if cleanup should be performed
88
+ */
89
+ export function decrementRefCount(): { shouldCleanup: boolean; count: number } {
90
+ state.refCount--;
91
+ const shouldCleanup = state.refCount <= 0 && state.unsubscribe !== null;
92
+ return { shouldCleanup, count: state.refCount };
93
+ }
94
+
95
+ /**
96
+ * Mark anonymous sign-in as in progress
97
+ */
98
+ export function startAnonymousSignIn(): boolean {
99
+ if (state.anonymousSignInInProgress) {
100
+ return false; // Already signing in
101
+ }
102
+ state.anonymousSignInInProgress = true;
103
+ return true;
104
+ }
105
+
106
+ /**
107
+ * Mark anonymous sign-in as complete
108
+ */
109
+ export function completeAnonymousSignIn(): void {
110
+ state.anonymousSignInInProgress = false;
111
+ }
112
+
113
+ /**
114
+ * Reset all state (for testing)
115
+ */
116
+ export function resetListenerState(): void {
117
+ if (state.unsubscribe) {
118
+ state.unsubscribe();
119
+ }
120
+ state.initialized = false;
121
+ state.refCount = 0;
122
+ state.initializationInProgress = false;
123
+ state.anonymousSignInInProgress = false;
124
+ state.unsubscribe = null;
125
+ }
@@ -87,24 +87,12 @@ export function createAuthInitModule(
87
87
  autoAnonymousSignIn,
88
88
  storageProvider,
89
89
  onUserConverted: async (anonymousId: string, authenticatedId: string) => {
90
- if (__DEV__) {
91
- console.log('[createAuthInitModule] User converted:', {
92
- anonymousId: anonymousId.slice(0, 8),
93
- authenticatedId: authenticatedId.slice(0, 8),
94
- });
95
- }
96
-
97
90
  // Restore purchases after conversion (if callback provided)
98
91
  if (onRestorePurchases) {
99
92
  try {
100
93
  await onRestorePurchases();
101
- if (__DEV__) {
102
- console.log('[createAuthInitModule] Purchases restored');
103
- }
104
- } catch (error) {
105
- if (__DEV__) {
106
- console.error('[createAuthInitModule] Restore failed:', error);
107
- }
94
+ } catch {
95
+ // Silently fail - purchase restoration errors are handled elsewhere
108
96
  }
109
97
  }
110
98
 
@@ -115,15 +103,8 @@ export function createAuthInitModule(
115
103
  },
116
104
  });
117
105
 
118
- if (__DEV__) {
119
- console.log('[createAuthInitModule] Auth initialized');
120
- }
121
-
122
106
  return true;
123
- } catch (error) {
124
- if (__DEV__) {
125
- console.error('[createAuthInitModule] Error:', error);
126
- }
107
+ } catch {
127
108
  return false;
128
109
  }
129
110
  },
@@ -47,12 +47,7 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
47
47
  const handleLogout = () => {
48
48
  alert.show(AlertType.WARNING, AlertMode.MODAL, logoutConfirmTitle, logoutConfirmMessage, {
49
49
  actions: [
50
- {
51
- id: "cancel",
52
- label: cancelText,
53
- style: "secondary",
54
- onPress: () => {},
55
- },
50
+ { id: "cancel", label: cancelText, style: "secondary", onPress: () => {} },
56
51
  {
57
52
  id: "confirm",
58
53
  label: logoutText,
@@ -60,10 +55,8 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
60
55
  onPress: async () => {
61
56
  try {
62
57
  await onLogout();
63
- } catch (error) {
64
- if (__DEV__) {
65
- console.error("[AccountActions] Logout failed:", error);
66
- }
58
+ } catch {
59
+ // Silently fail - logout error handling is managed elsewhere
67
60
  }
68
61
  },
69
62
  },
@@ -74,12 +67,7 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
74
67
  const handleDeleteAccount = () => {
75
68
  alert.show(AlertType.ERROR, AlertMode.MODAL, deleteConfirmTitle, deleteConfirmMessage, {
76
69
  actions: [
77
- {
78
- id: "cancel",
79
- label: cancelText,
80
- style: "secondary",
81
- onPress: () => {},
82
- },
70
+ { id: "cancel", label: cancelText, style: "secondary", onPress: () => {} },
83
71
  {
84
72
  id: "confirm",
85
73
  label: deleteAccountText,
@@ -99,40 +87,22 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
99
87
  return (
100
88
  <View style={styles.container}>
101
89
  {showChangePassword && onChangePassword && changePasswordText && (
102
- <TouchableOpacity
103
- style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]}
104
- onPress={onChangePassword}
105
- activeOpacity={0.7}
106
- >
90
+ <TouchableOpacity style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]} onPress={onChangePassword} activeOpacity={0.7}>
107
91
  <AtomicIcon name="key-outline" size="md" color="textPrimary" />
108
- <AtomicText style={actionButtonStyle.text} color="textPrimary">
109
- {changePasswordText}
110
- </AtomicText>
92
+ <AtomicText style={actionButtonStyle.text} color="textPrimary">{changePasswordText}</AtomicText>
111
93
  <AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
112
94
  </TouchableOpacity>
113
95
  )}
114
96
 
115
- <TouchableOpacity
116
- style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]}
117
- onPress={handleLogout}
118
- activeOpacity={0.7}
119
- >
97
+ <TouchableOpacity style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]} onPress={handleLogout} activeOpacity={0.7}>
120
98
  <AtomicIcon name="log-out-outline" size="md" color="error" />
121
- <AtomicText style={actionButtonStyle.text} color="error">
122
- {logoutText}
123
- </AtomicText>
99
+ <AtomicText style={actionButtonStyle.text} color="error">{logoutText}</AtomicText>
124
100
  <AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
125
101
  </TouchableOpacity>
126
102
 
127
- <TouchableOpacity
128
- style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]}
129
- onPress={handleDeleteAccount}
130
- activeOpacity={0.7}
131
- >
103
+ <TouchableOpacity style={[actionButtonStyle.container, { borderColor: tokens.colors.border }]} onPress={handleDeleteAccount} activeOpacity={0.7}>
132
104
  <AtomicIcon name="trash-outline" size="md" color="error" />
133
- <AtomicText style={actionButtonStyle.text} color="error">
134
- {deleteAccountText}
135
- </AtomicText>
105
+ <AtomicText style={actionButtonStyle.text} color="error">{deleteAccountText}</AtomicText>
136
106
  <AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
137
107
  </TouchableOpacity>
138
108
  </View>
@@ -140,7 +110,5 @@ export const AccountActions: React.FC<AccountActionsProps> = ({ config }) => {
140
110
  };
141
111
 
142
112
  const styles = StyleSheet.create({
143
- container: {
144
- gap: 12,
145
- },
113
+ container: { gap: 12 },
146
114
  });
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from "react";
1
+ import React from "react";
2
2
  import { View, TouchableOpacity, ScrollView } from "react-native";
3
3
  import {
4
4
  useAppDesignTokens,
@@ -63,22 +63,6 @@ export const AuthBottomSheet: React.FC<AuthBottomSheetProps> = ({
63
63
  handleAppleSignIn,
64
64
  } = useAuthBottomSheet({ socialConfig, onGoogleSignIn, onAppleSignIn, onAuthSuccess });
65
65
 
66
- useEffect(() => {
67
- if (__DEV__) {
68
- console.log("[AuthBottomSheet] Rendered with:", {
69
- mode,
70
- providersCount: providers.length,
71
- hasModalRef: !!modalRef.current,
72
- hasTermsUrl: !!termsUrl,
73
- hasPrivacyUrl: !!privacyUrl,
74
- });
75
- }
76
- }, [mode, providers.length, termsUrl, privacyUrl, modalRef]);
77
-
78
- if (__DEV__) {
79
- console.log("[AuthBottomSheet] Rendering...");
80
- }
81
-
82
66
  return (
83
67
  <BottomSheetModal
84
68
  ref={modalRef}
@@ -9,128 +9,98 @@ import { useAppDesignTokens, AtomicText, AtomicIcon, AtomicAvatar } from "@umitu
9
9
  import { ProfileBenefitsList } from "./ProfileBenefitsList";
10
10
 
11
11
  export interface ProfileSectionConfig {
12
- displayName?: string;
13
- userId?: string;
14
- isAnonymous: boolean;
15
- avatarUrl?: string;
16
- accountSettingsRoute?: string;
17
- benefits?: string[];
12
+ displayName?: string;
13
+ userId?: string;
14
+ isAnonymous: boolean;
15
+ avatarUrl?: string;
16
+ accountSettingsRoute?: string;
17
+ benefits?: string[];
18
18
  }
19
19
 
20
20
  export interface ProfileSectionProps {
21
- profile: ProfileSectionConfig;
22
- onPress?: () => void;
23
- onSignIn?: () => void;
24
- signInText?: string;
25
- anonymousText?: string;
21
+ profile: ProfileSectionConfig;
22
+ onPress?: () => void;
23
+ onSignIn?: () => void;
24
+ signInText?: string;
25
+ anonymousText?: string;
26
26
  }
27
27
 
28
28
  export const ProfileSection: React.FC<ProfileSectionProps> = ({
29
- profile,
30
- onPress,
31
- onSignIn,
32
- signInText,
33
- anonymousText,
29
+ profile,
30
+ onPress,
31
+ onSignIn,
32
+ signInText,
33
+ anonymousText,
34
34
  }) => {
35
- const tokens = useAppDesignTokens();
35
+ const tokens = useAppDesignTokens();
36
36
 
37
- const handlePress = () => {
38
- if (profile.isAnonymous && onSignIn) {
39
- onSignIn();
40
- } else if (onPress) {
41
- onPress();
42
- }
43
- };
37
+ const handlePress = () => {
38
+ if (profile.isAnonymous && onSignIn) {
39
+ onSignIn();
40
+ } else if (onPress) {
41
+ onPress();
42
+ }
43
+ };
44
44
 
45
- return (
46
- <TouchableOpacity
47
- style={[styles.container, { backgroundColor: tokens.colors.surface }]}
48
- onPress={handlePress}
49
- activeOpacity={0.7}
50
- disabled={!onPress && !onSignIn}
51
- >
52
- <View style={styles.content}>
53
- <View style={styles.avatarContainer}>
54
- <AtomicAvatar
55
- source={profile.avatarUrl ? { uri: profile.avatarUrl } : undefined}
56
- name={profile.displayName || (profile.isAnonymous ? anonymousText : signInText)}
57
- size="md"
58
- />
59
- </View>
45
+ return (
46
+ <TouchableOpacity
47
+ style={[styles.container, { backgroundColor: tokens.colors.surface }]}
48
+ onPress={handlePress}
49
+ activeOpacity={0.7}
50
+ disabled={!onPress && !onSignIn}
51
+ >
52
+ <View style={styles.content}>
53
+ <View style={styles.avatarContainer}>
54
+ <AtomicAvatar
55
+ source={profile.avatarUrl ? { uri: profile.avatarUrl } : undefined}
56
+ name={profile.displayName || (profile.isAnonymous ? anonymousText : signInText)}
57
+ size="md"
58
+ />
59
+ </View>
60
60
 
61
- <View style={styles.info}>
62
- <AtomicText
63
- type="titleMedium"
64
- color="textPrimary"
65
- numberOfLines={1}
66
- style={styles.displayName}
67
- >
68
- {profile.displayName}
69
- </AtomicText>
70
- {profile.userId && (
71
- <AtomicText
72
- type="bodySmall"
73
- color="textSecondary"
74
- numberOfLines={1}
75
- >
76
- {profile.userId}
77
- </AtomicText>
78
- )}
79
- </View>
61
+ <View style={styles.info}>
62
+ <AtomicText type="titleMedium" color="textPrimary" numberOfLines={1} style={styles.displayName}>
63
+ {profile.displayName}
64
+ </AtomicText>
65
+ {profile.userId && (
66
+ <AtomicText type="bodySmall" color="textSecondary" numberOfLines={1}>
67
+ {profile.userId}
68
+ </AtomicText>
69
+ )}
70
+ </View>
80
71
 
81
- {onPress && !profile.isAnonymous && (
82
- <AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
83
- )}
84
- </View>
72
+ {onPress && !profile.isAnonymous && (
73
+ <AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
74
+ )}
75
+ </View>
85
76
 
86
- {profile.isAnonymous && onSignIn && (
87
- <View style={[styles.ctaContainer, { borderTopColor: tokens.colors.border }]}>
88
- {profile.benefits && profile.benefits.length > 0 && (
89
- <ProfileBenefitsList benefits={profile.benefits} />
90
- )}
77
+ {profile.isAnonymous && onSignIn && (
78
+ <View style={[styles.ctaContainer, { borderTopColor: tokens.colors.border }]}>
79
+ {profile.benefits && profile.benefits.length > 0 && (
80
+ <ProfileBenefitsList benefits={profile.benefits} />
81
+ )}
91
82
 
92
- <TouchableOpacity
93
- style={[styles.ctaButton, { backgroundColor: tokens.colors.primary }]}
94
- onPress={onSignIn}
95
- activeOpacity={0.8}
96
- >
97
- <AtomicText type="labelLarge" style={{ color: tokens.colors.onPrimary }}>
98
- {signInText}
99
- </AtomicText>
100
- </TouchableOpacity>
101
- </View>
102
- )}
103
- </TouchableOpacity>
104
- );
83
+ <TouchableOpacity
84
+ style={[styles.ctaButton, { backgroundColor: tokens.colors.primary }]}
85
+ onPress={onSignIn}
86
+ activeOpacity={0.8}
87
+ >
88
+ <AtomicText type="labelLarge" style={{ color: tokens.colors.onPrimary }}>
89
+ {signInText}
90
+ </AtomicText>
91
+ </TouchableOpacity>
92
+ </View>
93
+ )}
94
+ </TouchableOpacity>
95
+ );
105
96
  };
106
97
 
107
98
  const styles = StyleSheet.create({
108
- container: {
109
- borderRadius: 12,
110
- padding: 16,
111
- marginBottom: 16,
112
- },
113
- content: {
114
- flexDirection: "row",
115
- alignItems: "center",
116
- },
117
- avatarContainer: {
118
- marginRight: 12,
119
- },
120
- info: {
121
- flex: 1,
122
- },
123
- displayName: {
124
- marginBottom: 2,
125
- },
126
- ctaContainer: {
127
- marginTop: 12,
128
- paddingTop: 12,
129
- borderTopWidth: 1,
130
- },
131
- ctaButton: {
132
- paddingVertical: 12,
133
- borderRadius: 8,
134
- alignItems: "center",
135
- },
99
+ container: { borderRadius: 12, padding: 16, marginBottom: 16 },
100
+ content: { flexDirection: "row", alignItems: "center" },
101
+ avatarContainer: { marginRight: 12 },
102
+ info: { flex: 1 },
103
+ displayName: { marginBottom: 2 },
104
+ ctaContainer: { marginTop: 12, paddingTop: 12, borderTopWidth: 1 },
105
+ ctaButton: { paddingVertical: 12, borderRadius: 8, alignItems: "center" },
136
106
  });