@mr.dj2u/library-registry 0.2.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.
Files changed (86) hide show
  1. package/README.md +3 -0
  2. package/assets/mds/env/convex.env.example +1 -0
  3. package/assets/mds/env/firebase.env.example +6 -0
  4. package/assets/mds/env/supabase.env.example +3 -0
  5. package/assets/mds/project/auth-base.md +17 -0
  6. package/assets/mds/project/auth-convex.md +33 -0
  7. package/assets/mds/project/auth-firebase.md +22 -0
  8. package/assets/mds/project/auth-supabase.md +46 -0
  9. package/assets/mds/src/app/(auth)/reset-password.tsx +5 -0
  10. package/assets/mds/src/app/(auth)/sign-in.tsx +5 -0
  11. package/assets/mds/src/app/(auth)/sign-up.tsx +5 -0
  12. package/assets/mds/src/app/legal/updates.tsx +1 -0
  13. package/assets/mds/src/app/onboarding/complete.tsx +2 -0
  14. package/assets/mds/src/app/onboarding/features.tsx +1 -0
  15. package/assets/mds/src/app/onboarding/legal.tsx +2 -0
  16. package/assets/mds/src/app/onboarding.tsx +1 -1
  17. package/assets/mds/src/app/settings.tsx +10 -1
  18. package/assets/mds/src/db/adapter.ts +195 -0
  19. package/assets/mds/src/db/firebase.ts +77 -0
  20. package/assets/mds/src/db/index.firebase.ts +19 -0
  21. package/assets/mds/src/db/index.supabase.ts +22 -0
  22. package/assets/mds/src/db/supabase.ts +337 -0
  23. package/assets/mds/src/features/auth/adapters/base-auth-adapter.tsx +82 -0
  24. package/assets/mds/src/features/auth/adapters/convex-auth-adapter.tsx +156 -0
  25. package/assets/mds/src/features/auth/adapters/firebase-auth-adapter.tsx +147 -0
  26. package/assets/mds/src/features/auth/adapters/supabase-auth-adapter.tsx +166 -0
  27. package/assets/mds/src/features/auth/auth-guard-logic.ts +19 -0
  28. package/assets/mds/src/features/auth/auth-guard.tsx +77 -0
  29. package/assets/mds/src/features/auth/auth-provider.tsx +60 -0
  30. package/assets/mds/src/features/auth/auth-screen.tsx +265 -0
  31. package/assets/mds/src/features/auth/auth-types.ts +45 -0
  32. package/assets/mds/src/features/exposition/data-screen.tsx +4 -2
  33. package/assets/mds/src/features/exposition/expo-sdk-56-screen.tsx +119 -76
  34. package/assets/mds/src/features/exposition/exposition-screen.tsx +10 -11
  35. package/assets/mds/src/features/exposition/stylist-screen.tsx +62 -11
  36. package/assets/mds/src/features/legal/legal-acceptance-adapter.ts +190 -0
  37. package/assets/mds/src/features/legal/legal-acceptance-config.ts +79 -0
  38. package/assets/mds/src/features/legal/legal-agreement-screen.tsx +9 -4
  39. package/assets/mds/src/features/legal/legal-document-modal.tsx +7 -3
  40. package/assets/mds/src/features/legal/legal-document-view.tsx +1 -0
  41. package/assets/mds/src/features/legal/legal-documents.ts +17 -3
  42. package/assets/mds/src/features/legal/legal-update-screen.tsx +297 -0
  43. package/assets/mds/src/features/legal/use-legal-acceptance.ts +1 -38
  44. package/assets/mds/src/features/onboarding/complete-screen.tsx +138 -0
  45. package/assets/mds/src/features/onboarding/features-screen.tsx +129 -0
  46. package/assets/mds/src/features/onboarding/legal-review-screen.tsx +242 -0
  47. package/assets/mds/src/features/onboarding/onboarding-config-with-legal.ts +104 -0
  48. package/assets/mds/src/features/onboarding/onboarding-config.ts +104 -0
  49. package/assets/mds/src/features/onboarding/onboarding-persistence-sync.tsx +1 -0
  50. package/assets/mds/src/features/onboarding/welcome-screen.tsx +130 -0
  51. package/assets/mds/src/features/onboarding-state/adapters/memory-onboarding-state-adapter.ts +15 -0
  52. package/assets/mds/src/features/onboarding-state/adapters/supabase-onboarding-state-adapter.ts +45 -0
  53. package/assets/mds/src/features/onboarding-state/adapters/zustand-onboarding-state-adapter.ts +16 -0
  54. package/assets/mds/src/features/onboarding-state/adapters/zustand-supabase-onboarding-state-adapter.ts +53 -0
  55. package/assets/mds/src/features/onboarding-state/onboarding-state-core.ts +108 -0
  56. package/assets/mds/src/features/onboarding-state/onboarding-state-memory.ts +106 -0
  57. package/assets/mds/src/features/onboarding-state/onboarding-state-supabase.ts +246 -0
  58. package/assets/mds/src/features/onboarding-state/onboarding-state-types.ts +64 -0
  59. package/assets/mds/src/features/onboarding-state/onboarding-state-zustand-supabase.ts +120 -0
  60. package/assets/mds/src/features/onboarding-state/onboarding-state-zustand.ts +71 -0
  61. package/assets/mds/src/features/onboarding-state/onboarding-state.ts +22 -0
  62. package/assets/mds/src/features/onboarding-state/onboarding-store.ts +39 -0
  63. package/assets/mds/src/features/settings/settings-screen-logic.ts +122 -0
  64. package/assets/mds/src/features/settings/settings-screen.tsx +274 -28
  65. package/assets/mds/src/services/convex.ts +40 -0
  66. package/assets/mds/src/services/firebase.ts +70 -0
  67. package/assets/mds/src/services/supabase.ts +50 -0
  68. package/assets/mds/src/theme/color-utils.ts +42 -0
  69. package/assets/mds/src/theme/provider.tsx +15 -5
  70. package/assets/mds/src/types/database.ts +13 -0
  71. package/assets/mds/supabase/migrations/0001_mds_auth_onboarding.sql +107 -0
  72. package/dist/catalog.d.ts.map +1 -1
  73. package/dist/catalog.js +444 -29
  74. package/dist/catalog.js.map +1 -1
  75. package/dist/types.d.ts +5 -1
  76. package/dist/types.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/assets/mds/src/app/onboarding/account-setup.tsx +0 -1
  79. package/assets/mds/src/app/onboarding/agreement.tsx +0 -1
  80. package/assets/mds/src/app/onboarding/terms.tsx +0 -1
  81. package/assets/mds/src/features/onboarding/account-setup-screen.tsx +0 -54
  82. package/assets/mds/src/features/onboarding/agreement-screen.tsx +0 -6
  83. package/assets/mds/src/features/onboarding/components/legal-document-view.tsx +0 -103
  84. package/assets/mds/src/features/onboarding/legal-documents.ts +0 -80
  85. package/assets/mds/src/features/onboarding/onboarding-screen.tsx +0 -167
  86. package/assets/mds/src/features/onboarding/terms-screen.tsx +0 -6
@@ -0,0 +1,297 @@
1
+ import { useRouter } from 'expo-router';
2
+ import { useMemo, useState } from 'react';
3
+ import { Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
4
+
5
+ import { getReadableTextColor } from '../../theme/color-utils';
6
+ import { useAppTheme } from '../../theme/provider';
7
+ import { LegalDocumentModal } from './legal-document-modal';
8
+ import { getLegalDocument, type LegalDocumentId } from './legal-documents';
9
+ import {
10
+ useLegalUpdateGateSnapshot,
11
+ type RequiredLegalDocument,
12
+ } from './legal-acceptance-adapter';
13
+
14
+ function LegalUpdateRow({
15
+ document,
16
+ saving,
17
+ onOpen,
18
+ }: {
19
+ document: RequiredLegalDocument;
20
+ saving: boolean;
21
+ onOpen: () => void;
22
+ }) {
23
+ const theme = useAppTheme();
24
+ const colors = theme.activeColors;
25
+
26
+ return (
27
+ <Pressable
28
+ accessibilityRole="button"
29
+ onPress={onOpen}
30
+ style={[
31
+ styles.documentCard,
32
+ {
33
+ backgroundColor: colors.surface,
34
+ borderColor: colors.primary,
35
+ borderRadius: theme.layout.radius,
36
+ },
37
+ ]}>
38
+ <View style={styles.documentText}>
39
+ <Text style={[styles.documentTitle, { color: colors.text }]}>{document.title}</Text>
40
+ <Text style={[styles.documentMeta, { color: colors.text }]}>
41
+ Version {document.acceptanceVersion}
42
+ </Text>
43
+ <Text style={[styles.documentSummary, { color: colors.text }]}>
44
+ {document.changeSummary}
45
+ </Text>
46
+ </View>
47
+ <Text style={[styles.documentAction, { color: colors.primary }]}>
48
+ {saving ? 'Saving' : 'Review'}
49
+ </Text>
50
+ </Pressable>
51
+ );
52
+ }
53
+
54
+ export default function LegalUpdateScreen() {
55
+ const router = useRouter();
56
+ const theme = useAppTheme();
57
+ const colors = theme.activeColors;
58
+ const primaryForeground = getReadableTextColor(colors.primary, theme.colors.light.text);
59
+ const disabledForeground = getReadableTextColor(colors.surface, theme.colors.light.text);
60
+ const [activeDocumentId, setActiveDocumentId] = useState<LegalDocumentId | null>(null);
61
+ const { snapshot, refresh, acceptDocument, savingDocumentId } = useLegalUpdateGateSnapshot();
62
+ const activeRequiredDocument = useMemo(
63
+ () => snapshot.requiredDocuments.find((document) => document.documentId === activeDocumentId),
64
+ [activeDocumentId, snapshot.requiredDocuments],
65
+ );
66
+ const hasRequiredDocuments = snapshot.requiredDocuments.length > 0;
67
+ const isChecking = snapshot.status === 'checking';
68
+ const isComplete = snapshot.status === 'complete';
69
+ const canContinue = isComplete && !isChecking;
70
+
71
+ const closeModal = () => setActiveDocumentId(null);
72
+ const acceptActiveDocument = async () => {
73
+ if (!activeRequiredDocument) {
74
+ closeModal();
75
+ return;
76
+ }
77
+
78
+ await acceptDocument(activeRequiredDocument);
79
+ closeModal();
80
+ };
81
+ const continueToApp = () => {
82
+ if (canContinue) {
83
+ router.replace('/');
84
+ }
85
+ };
86
+
87
+ return (
88
+ <ScrollView
89
+ contentInsetAdjustmentBehavior="automatic"
90
+ contentContainerStyle={styles.content}
91
+ style={[styles.screen, { backgroundColor: colors.background }]}>
92
+ <View style={styles.header}>
93
+ <Text style={[styles.kicker, { color: colors.primary }]}>Legal Update</Text>
94
+ <Text
95
+ style={[
96
+ styles.title,
97
+ {
98
+ color: colors.text,
99
+ fontFamily: theme.typography.fontTitle,
100
+ },
101
+ ]}>
102
+ Review required document updates
103
+ </Text>
104
+ <Text style={[styles.body, { color: colors.text }]}>
105
+ Review and accept the current material legal document versions before entering the app.
106
+ </Text>
107
+ </View>
108
+
109
+ {snapshot.status === 'error' ? (
110
+ <View
111
+ style={[
112
+ styles.statusCard,
113
+ {
114
+ backgroundColor: colors.surface,
115
+ borderColor: colors.warning,
116
+ borderRadius: theme.layout.radius,
117
+ },
118
+ ]}>
119
+ <Text style={[styles.statusTitle, { color: colors.text }]}>Unable to check acceptance</Text>
120
+ <Text style={[styles.statusBody, { color: colors.text }]}>
121
+ {snapshot.error ?? 'Try again before continuing.'}
122
+ </Text>
123
+ <Pressable
124
+ accessibilityRole="button"
125
+ onPress={() => void refresh()}
126
+ style={[
127
+ styles.secondaryButton,
128
+ { borderColor: colors.primary, borderRadius: theme.layout.radius },
129
+ ]}>
130
+ <Text style={[styles.secondaryButtonText, { color: colors.primary }]}>Retry</Text>
131
+ </Pressable>
132
+ </View>
133
+ ) : null}
134
+
135
+ {isChecking ? (
136
+ <Text style={[styles.statusBody, { color: colors.text }]}>Checking legal acceptance...</Text>
137
+ ) : null}
138
+
139
+ {!isChecking && !hasRequiredDocuments && snapshot.status !== 'error' ? (
140
+ <View
141
+ style={[
142
+ styles.statusCard,
143
+ {
144
+ backgroundColor: colors.surface,
145
+ borderColor: colors.success,
146
+ borderRadius: theme.layout.radius,
147
+ },
148
+ ]}>
149
+ <Text style={[styles.statusTitle, { color: colors.text }]}>
150
+ Legal documents are current
151
+ </Text>
152
+ <Text style={[styles.statusBody, { color: colors.text }]}>
153
+ No required legal updates are waiting on this account.
154
+ </Text>
155
+ </View>
156
+ ) : null}
157
+
158
+ {hasRequiredDocuments ? (
159
+ <View style={styles.stack}>
160
+ {snapshot.requiredDocuments.map((document) => (
161
+ <LegalUpdateRow
162
+ document={document}
163
+ key={`${document.documentId}-${document.acceptanceVersion}`}
164
+ onOpen={() => setActiveDocumentId(document.documentId)}
165
+ saving={savingDocumentId === document.documentId}
166
+ />
167
+ ))}
168
+ </View>
169
+ ) : null}
170
+
171
+ <Pressable
172
+ accessibilityRole="button"
173
+ disabled={!canContinue}
174
+ onPress={continueToApp}
175
+ style={[
176
+ styles.primaryButton,
177
+ {
178
+ backgroundColor: canContinue ? colors.primary : colors.surface,
179
+ borderRadius: theme.layout.radius,
180
+ },
181
+ ]}>
182
+ <Text
183
+ style={[
184
+ styles.primaryButtonText,
185
+ { color: canContinue ? primaryForeground : disabledForeground },
186
+ ]}>
187
+ Continue to app
188
+ </Text>
189
+ </Pressable>
190
+
191
+ {activeRequiredDocument ? (
192
+ <LegalDocumentModal
193
+ documentId={activeRequiredDocument.documentId}
194
+ onClose={closeModal}
195
+ onPrimaryAction={() => void acceptActiveDocument()}
196
+ primaryActionLabel={`Accept ${getLegalDocument(activeRequiredDocument.documentId).title}`}
197
+ visible
198
+ />
199
+ ) : null}
200
+ </ScrollView>
201
+ );
202
+ }
203
+
204
+ const styles = StyleSheet.create({
205
+ screen: {
206
+ flex: 1,
207
+ },
208
+ content: {
209
+ flexGrow: 1,
210
+ gap: 18,
211
+ justifyContent: 'center',
212
+ padding: 20,
213
+ paddingTop: Platform.OS === 'web' ? 84 : 28,
214
+ },
215
+ header: {
216
+ gap: 8,
217
+ },
218
+ kicker: {
219
+ fontSize: 13,
220
+ fontWeight: '900',
221
+ textTransform: 'uppercase',
222
+ },
223
+ title: {
224
+ fontSize: 28,
225
+ fontWeight: '900',
226
+ lineHeight: 34,
227
+ maxWidth: 780,
228
+ },
229
+ body: {
230
+ fontSize: 15,
231
+ lineHeight: 22,
232
+ maxWidth: 720,
233
+ },
234
+ stack: {
235
+ gap: 12,
236
+ },
237
+ documentCard: {
238
+ alignItems: 'center',
239
+ borderWidth: 1,
240
+ flexDirection: 'row',
241
+ gap: 12,
242
+ padding: 16,
243
+ },
244
+ documentText: {
245
+ flex: 1,
246
+ gap: 4,
247
+ },
248
+ documentTitle: {
249
+ fontSize: 17,
250
+ fontWeight: '900',
251
+ },
252
+ documentMeta: {
253
+ fontSize: 13,
254
+ lineHeight: 18,
255
+ },
256
+ documentSummary: {
257
+ fontSize: 14,
258
+ lineHeight: 20,
259
+ },
260
+ documentAction: {
261
+ fontSize: 13,
262
+ fontWeight: '900',
263
+ },
264
+ statusCard: {
265
+ borderWidth: 1,
266
+ gap: 8,
267
+ padding: 16,
268
+ },
269
+ statusTitle: {
270
+ fontSize: 17,
271
+ fontWeight: '900',
272
+ },
273
+ statusBody: {
274
+ fontSize: 14,
275
+ lineHeight: 20,
276
+ },
277
+ primaryButton: {
278
+ alignItems: 'center',
279
+ marginTop: 4,
280
+ paddingVertical: 15,
281
+ },
282
+ primaryButtonText: {
283
+ fontSize: 15,
284
+ fontWeight: '900',
285
+ },
286
+ secondaryButton: {
287
+ alignItems: 'center',
288
+ alignSelf: 'flex-start',
289
+ borderWidth: 1,
290
+ paddingHorizontal: 16,
291
+ paddingVertical: 10,
292
+ },
293
+ secondaryButtonText: {
294
+ fontSize: 14,
295
+ fontWeight: '900',
296
+ },
297
+ });
@@ -1,42 +1,5 @@
1
- import { useMemo, useState } from 'react';
2
-
3
1
  import type { LegalDocumentId } from './legal-documents';
4
2
 
5
3
  export type LegalAcceptanceRecord = Record<LegalDocumentId, boolean>;
6
4
 
7
- const defaultAcceptance: LegalAcceptanceRecord = {
8
- terms: false,
9
- privacy: false,
10
- };
11
-
12
- export function useLegalAcceptance(initialAcceptance: Partial<LegalAcceptanceRecord> = {}) {
13
- const [acceptedDocuments, setAcceptedDocuments] = useState<LegalAcceptanceRecord>({
14
- ...defaultAcceptance,
15
- ...initialAcceptance,
16
- });
17
-
18
- const hasAcceptedRequiredDocuments = useMemo(
19
- () => acceptedDocuments.terms && acceptedDocuments.privacy,
20
- [acceptedDocuments.privacy, acceptedDocuments.terms],
21
- );
22
-
23
- const acceptDocument = (documentId: LegalDocumentId) => {
24
- setAcceptedDocuments((current) => ({ ...current, [documentId]: true }));
25
- };
26
-
27
- const revokeDocument = (documentId: LegalDocumentId) => {
28
- setAcceptedDocuments((current) => ({ ...current, [documentId]: false }));
29
- };
30
-
31
- const resetLegalAcceptance = () => {
32
- setAcceptedDocuments(defaultAcceptance);
33
- };
34
-
35
- return {
36
- acceptedDocuments,
37
- hasAcceptedRequiredDocuments,
38
- acceptDocument,
39
- revokeDocument,
40
- resetLegalAcceptance,
41
- };
42
- }
5
+ export { useLegalAcceptance } from './legal-acceptance-adapter';
@@ -0,0 +1,138 @@
1
+ import { useRouter } from 'expo-router';
2
+ import { useState } from 'react';
3
+ import { Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
4
+
5
+ import { getReadableTextColor } from '../../theme/color-utils';
6
+ import { useAppTheme } from '../../theme/provider';
7
+ import { markOnboardingComplete } from '../onboarding-state/onboarding-state';
8
+ import { onboardingConfig } from './onboarding-config';
9
+
10
+ export default function OnboardingCompleteScreen() {
11
+ const router = useRouter();
12
+ const theme = useAppTheme();
13
+ const colors = theme.activeColors;
14
+ const primaryForeground = getReadableTextColor(colors.primary, theme.colors.light.text);
15
+ const [isSaving, setIsSaving] = useState(false);
16
+ const [completionError, setCompletionError] = useState<string | null>(null);
17
+
18
+ const finishOnboarding = async () => {
19
+ if (isSaving) {
20
+ return;
21
+ }
22
+ setIsSaving(true);
23
+ setCompletionError(null);
24
+ try {
25
+ await markOnboardingComplete();
26
+ setTimeout(() => router.replace(onboardingConfig.completion.route), 0);
27
+ } catch (error) {
28
+ setCompletionError(
29
+ error instanceof Error ? error.message : 'Unable to finish onboarding. Please try again.'
30
+ );
31
+ } finally {
32
+ setIsSaving(false);
33
+ }
34
+ };
35
+
36
+ return (
37
+ <ScrollView
38
+ contentInsetAdjustmentBehavior="automatic"
39
+ contentContainerStyle={styles.content}
40
+ style={[styles.screen, { backgroundColor: colors.background }]}>
41
+ <View
42
+ style={[
43
+ styles.panel,
44
+ {
45
+ backgroundColor: colors.surface,
46
+ borderColor: colors.primary,
47
+ borderRadius: theme.layout.radius,
48
+ },
49
+ ]}>
50
+ <Text style={[styles.kicker, { color: colors.primary }]}>
51
+ {onboardingConfig.completion.mode}
52
+ </Text>
53
+ <Text
54
+ style={[
55
+ styles.title,
56
+ {
57
+ color: colors.text,
58
+ fontFamily: theme.typography.fontTitle,
59
+ },
60
+ ]}>
61
+ {onboardingConfig.completeTitle}
62
+ </Text>
63
+ <Text style={[styles.body, { color: colors.text }]}>{onboardingConfig.completeBody}</Text>
64
+ <Text style={[styles.helper, { color: colors.text }]}>
65
+ {onboardingConfig.completion.helperText}
66
+ </Text>
67
+ {completionError ? (
68
+ <Text style={[styles.errorText, { color: colors.warning }]}>{completionError}</Text>
69
+ ) : null}
70
+ <Pressable
71
+ accessibilityRole="button"
72
+ disabled={isSaving}
73
+ onPress={() => void finishOnboarding()}
74
+ style={[
75
+ styles.primaryButton,
76
+ { backgroundColor: colors.primary, borderRadius: theme.layout.radius },
77
+ ]}>
78
+ <Text style={[styles.primaryButtonText, { color: primaryForeground }]}>
79
+ {isSaving ? 'Completing...' : onboardingConfig.completion.label}
80
+ </Text>
81
+ </Pressable>
82
+ </View>
83
+ </ScrollView>
84
+ );
85
+ }
86
+
87
+ const styles = StyleSheet.create({
88
+ screen: {
89
+ flex: 1,
90
+ },
91
+ content: {
92
+ flexGrow: 1,
93
+ justifyContent: 'center',
94
+ padding: 20,
95
+ paddingTop: Platform.OS === 'web' ? 84 : 28,
96
+ },
97
+ panel: {
98
+ alignSelf: 'center',
99
+ borderWidth: 1,
100
+ gap: 12,
101
+ maxWidth: 760,
102
+ padding: 18,
103
+ width: '100%',
104
+ },
105
+ kicker: {
106
+ fontSize: 12,
107
+ fontWeight: '900',
108
+ textTransform: 'uppercase',
109
+ },
110
+ title: {
111
+ fontSize: 28,
112
+ fontWeight: '900',
113
+ lineHeight: 34,
114
+ },
115
+ body: {
116
+ fontSize: 15,
117
+ lineHeight: 22,
118
+ },
119
+ helper: {
120
+ fontSize: 13,
121
+ lineHeight: 19,
122
+ opacity: 0.86,
123
+ },
124
+ errorText: {
125
+ fontSize: 13,
126
+ fontWeight: '800',
127
+ lineHeight: 19,
128
+ },
129
+ primaryButton: {
130
+ alignItems: 'center',
131
+ marginTop: 4,
132
+ paddingVertical: 15,
133
+ },
134
+ primaryButtonText: {
135
+ fontSize: 15,
136
+ fontWeight: '900',
137
+ },
138
+ });
@@ -0,0 +1,129 @@
1
+ import { Link } from 'expo-router';
2
+ import { Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
3
+
4
+ import { getReadableTextColor } from '../../theme/color-utils';
5
+ import { useAppTheme } from '../../theme/provider';
6
+ import { onboardingConfig } from './onboarding-config';
7
+
8
+ export default function OnboardingFeaturesScreen() {
9
+ const theme = useAppTheme();
10
+ const colors = theme.activeColors;
11
+ const primaryForeground = getReadableTextColor(colors.primary, theme.colors.light.text);
12
+
13
+ return (
14
+ <ScrollView
15
+ contentInsetAdjustmentBehavior="automatic"
16
+ contentContainerStyle={styles.content}
17
+ style={[styles.screen, { backgroundColor: colors.background }]}>
18
+ <View style={styles.header}>
19
+ <Text style={[styles.kicker, { color: colors.primary }]}>
20
+ {onboardingConfig.featuresEyebrow}
21
+ </Text>
22
+ <Text
23
+ style={[
24
+ styles.title,
25
+ {
26
+ color: colors.text,
27
+ fontFamily: theme.typography.fontTitle,
28
+ },
29
+ ]}>
30
+ {onboardingConfig.featuresTitle}
31
+ </Text>
32
+ <Text style={[styles.body, { color: colors.text }]}>{onboardingConfig.featuresBody}</Text>
33
+ </View>
34
+
35
+ <View style={styles.stack}>
36
+ {onboardingConfig.featureHighlights.map((feature) => (
37
+ <View
38
+ key={feature.id}
39
+ style={[
40
+ styles.featureCard,
41
+ {
42
+ backgroundColor: colors.surface,
43
+ borderColor: colors.primary,
44
+ borderRadius: theme.layout.radius,
45
+ },
46
+ ]}>
47
+ {feature.badge ? (
48
+ <Text style={[styles.badge, { color: colors.primary }]}>{feature.badge}</Text>
49
+ ) : null}
50
+ <Text style={[styles.featureTitle, { color: colors.text }]}>{feature.title}</Text>
51
+ <Text style={[styles.featureBody, { color: colors.text }]}>{feature.body}</Text>
52
+ </View>
53
+ ))}
54
+ </View>
55
+
56
+ <Link href={onboardingConfig.nextRouteAfterFeatures} asChild>
57
+ <Pressable
58
+ accessibilityRole="button"
59
+ style={StyleSheet.flatten([
60
+ styles.primaryButton,
61
+ { backgroundColor: colors.primary, borderRadius: theme.layout.radius },
62
+ ])}>
63
+ <Text style={[styles.primaryButtonText, { color: primaryForeground }]}>Continue</Text>
64
+ </Pressable>
65
+ </Link>
66
+ </ScrollView>
67
+ );
68
+ }
69
+
70
+ const styles = StyleSheet.create({
71
+ screen: {
72
+ flex: 1,
73
+ },
74
+ content: {
75
+ flexGrow: 1,
76
+ gap: 18,
77
+ justifyContent: 'center',
78
+ padding: 20,
79
+ paddingTop: Platform.OS === 'web' ? 84 : 28,
80
+ },
81
+ header: {
82
+ gap: 8,
83
+ },
84
+ kicker: {
85
+ fontSize: 13,
86
+ fontWeight: '900',
87
+ textTransform: 'uppercase',
88
+ },
89
+ title: {
90
+ fontSize: 28,
91
+ fontWeight: '900',
92
+ lineHeight: 34,
93
+ },
94
+ body: {
95
+ fontSize: 15,
96
+ lineHeight: 22,
97
+ maxWidth: 720,
98
+ },
99
+ stack: {
100
+ gap: 12,
101
+ },
102
+ featureCard: {
103
+ borderWidth: 1,
104
+ gap: 7,
105
+ padding: 16,
106
+ },
107
+ badge: {
108
+ fontSize: 11,
109
+ fontWeight: '900',
110
+ textTransform: 'uppercase',
111
+ },
112
+ featureTitle: {
113
+ fontSize: 17,
114
+ fontWeight: '900',
115
+ },
116
+ featureBody: {
117
+ fontSize: 14,
118
+ lineHeight: 20,
119
+ },
120
+ primaryButton: {
121
+ alignItems: 'center',
122
+ marginTop: 4,
123
+ paddingVertical: 15,
124
+ },
125
+ primaryButtonText: {
126
+ fontSize: 15,
127
+ fontWeight: '900',
128
+ },
129
+ });