@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
@@ -13,6 +13,7 @@ import {
13
13
  Text,
14
14
  TextInput,
15
15
  type TextStyle,
16
+ useColorScheme,
16
17
  View,
17
18
  } from 'react-native';
18
19
  import ColorPicker, { HueSlider, Panel1, Preview } from 'reanimated-color-picker';
@@ -29,7 +30,7 @@ import defaultThemeTokens, {
29
30
  type StylistSemanticFamilies,
30
31
  type StylistThemeTokens,
31
32
  } from '../../theme/tokens';
32
- import { useSetAppTheme } from '../../theme/provider';
33
+ import { readSystemScheme, useSetAppTheme } from '../../theme/provider';
33
34
 
34
35
  type SemanticColorKey = keyof StylistSemanticFamilies;
35
36
  type PaletteColorKey = keyof StylistColorPalette;
@@ -864,21 +865,45 @@ function ensureWebFontLoaded(fontFamily: string): void {
864
865
  loadedWebFonts.add(normalized);
865
866
  }
866
867
 
868
+ function withPreviewScheme(
869
+ theme: ExtendedStylistThemeTokens,
870
+ scheme: StylistColorScheme
871
+ ): ExtendedStylistThemeTokens {
872
+ if (theme.colorSystem.previewScheme === scheme) {
873
+ return theme;
874
+ }
875
+
876
+ return {
877
+ ...theme,
878
+ colorSystem: {
879
+ ...theme.colorSystem,
880
+ previewScheme: scheme,
881
+ },
882
+ };
883
+ }
884
+
885
+ function createInitialStylistTheme(): ExtendedStylistThemeTokens {
886
+ const theme = withFontFamilyAlias(
887
+ normalizeThemeTypography(
888
+ reconcileTheme(stylistThemeTokens, 'picker', defaultBgFamilies, defaultBgFamilyShades)
889
+ )
890
+ );
891
+ const systemScheme = readSystemScheme();
892
+ return systemScheme ? withPreviewScheme(theme, systemScheme) : theme;
893
+ }
894
+
867
895
  export default function StylistScreen() {
868
896
  const insets = useSafeAreaInsets();
869
897
  const setAppTheme = useSetAppTheme();
898
+ const systemScheme = useColorScheme();
899
+ const userOverrodePreview = useRef(false);
900
+ const previewSchemeInitializedFromSystem = useRef(readSystemScheme() !== null);
870
901
  const [colorInputMode, setColorInputMode] = useState<ColorInputMode>('picker');
871
902
  const [bgFamilies, setBgFamilies] =
872
903
  useState<Record<StylistColorScheme, PaletteFamilies>>(defaultBgFamilies);
873
904
  const [bgFamilyShades, setBgFamilyShades] =
874
905
  useState<Record<StylistColorScheme, PaletteShades>>(defaultBgFamilyShades);
875
- const [theme, setTheme] = useState<ExtendedStylistThemeTokens>(
876
- withFontFamilyAlias(
877
- normalizeThemeTypography(
878
- reconcileTheme(stylistThemeTokens, 'picker', defaultBgFamilies, defaultBgFamilyShades)
879
- )
880
- )
881
- );
906
+ const [theme, setTheme] = useState<ExtendedStylistThemeTokens>(createInitialStylistTheme);
882
907
  const [selectedColor, setSelectedColor] = useState<PaletteColorKey>('primary');
883
908
  const [pickerHexDraft, setPickerHexDraft] = useState('#');
884
909
  const [livePickerPreview, setLivePickerPreview] = useState<LivePickerPreview | null>(null);
@@ -1007,13 +1032,27 @@ export default function StylistScreen() {
1007
1032
  return;
1008
1033
  }
1009
1034
  if (payload.theme) {
1010
- const nextTheme = withFontFamilyAlias(
1035
+ const hydratedTheme = withFontFamilyAlias(
1011
1036
  normalizeThemeTypography(
1012
1037
  reconcileTheme(payload.theme, 'picker', defaultBgFamilies, defaultBgFamilyShades)
1013
1038
  )
1014
1039
  );
1015
- setTheme((prev) => (areThemesEqual(prev, nextTheme) ? prev : nextTheme));
1016
- setAppTheme(nextTheme);
1040
+ const systemSchemeAtLoad = readSystemScheme();
1041
+ setTheme((prev) => {
1042
+ const resolvedScheme = userOverrodePreview.current
1043
+ ? prev.colorSystem.previewScheme
1044
+ : (systemSchemeAtLoad ?? prev.colorSystem.previewScheme);
1045
+ if (!userOverrodePreview.current && systemSchemeAtLoad) {
1046
+ previewSchemeInitializedFromSystem.current = true;
1047
+ }
1048
+ const nextTheme = withPreviewScheme(hydratedTheme, resolvedScheme);
1049
+ return areThemesEqual(prev, nextTheme) ? prev : nextTheme;
1050
+ });
1051
+ setAppTheme(
1052
+ systemSchemeAtLoad && !userOverrodePreview.current
1053
+ ? withPreviewScheme(hydratedTheme, systemSchemeAtLoad)
1054
+ : hydratedTheme
1055
+ );
1017
1056
  setExplicitFontRoles({
1018
1057
  fontDisplay: true,
1019
1058
  fontTitle: true,
@@ -1054,6 +1093,17 @@ export default function StylistScreen() {
1054
1093
  };
1055
1094
  }, [setAppTheme]);
1056
1095
 
1096
+ useEffect(() => {
1097
+ if (userOverrodePreview.current || previewSchemeInitializedFromSystem.current) {
1098
+ return;
1099
+ }
1100
+ if (systemScheme !== 'light' && systemScheme !== 'dark') {
1101
+ return;
1102
+ }
1103
+ previewSchemeInitializedFromSystem.current = true;
1104
+ setTheme((prev) => withPreviewScheme(prev, systemScheme));
1105
+ }, [systemScheme]);
1106
+
1057
1107
  const activeScheme = theme.colorSystem.previewScheme;
1058
1108
  const basePreviewColors = theme.colors[activeScheme];
1059
1109
  const baseEditablePalette =
@@ -1353,6 +1403,7 @@ export default function StylistScreen() {
1353
1403
  }
1354
1404
 
1355
1405
  function updatePreviewScheme(scheme: StylistColorScheme) {
1406
+ userOverrodePreview.current = true;
1356
1407
  updateTheme((prev) => ({
1357
1408
  ...prev,
1358
1409
  colorSystem: {
@@ -0,0 +1,190 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+
3
+ import {
4
+ configureLegalAcceptanceAdapter as configureSharedLegalAcceptanceAdapter,
5
+ getLegalAcceptanceUserId,
6
+ getLegalAcceptanceAdapter,
7
+ memoryLegalAcceptanceAdapter as sharedMemoryLegalAcceptanceAdapter,
8
+ notifyLegalAcceptanceChanged,
9
+ setLegalAcceptanceUserId,
10
+ subscribeToLegalAcceptanceChanges,
11
+ } from './legal-acceptance-config';
12
+ import { legalDocuments, type LegalDocument, type LegalDocumentId } from './legal-documents';
13
+
14
+ export type LegalGateStatus = 'checking' | 'needs-legal' | 'complete' | 'error';
15
+
16
+ export interface RequiredLegalDocument {
17
+ documentId: LegalDocumentId;
18
+ acceptanceVersion: string;
19
+ title: string;
20
+ changeSummary: string;
21
+ }
22
+
23
+ export interface LegalAcceptanceSnapshot {
24
+ status: LegalGateStatus;
25
+ requiredDocuments: RequiredLegalDocument[];
26
+ acceptedDocumentKeys: string[];
27
+ error?: string;
28
+ }
29
+
30
+ export interface LegalAcceptanceAdapter {
31
+ loadRequiredLegalAcceptances(
32
+ requiredDocuments: RequiredLegalDocument[],
33
+ userId?: string,
34
+ ): Promise<LegalAcceptanceSnapshot>;
35
+ acceptLegalDocument(
36
+ document: RequiredLegalDocument,
37
+ input?: { userId?: string; flowId?: string; flowVersion?: number },
38
+ ): Promise<void>;
39
+ }
40
+
41
+ function toRequiredLegalDocument(document: LegalDocument): RequiredLegalDocument {
42
+ return {
43
+ documentId: document.id,
44
+ acceptanceVersion: document.acceptanceVersion,
45
+ title: document.title,
46
+ changeSummary: document.changeSummary,
47
+ };
48
+ }
49
+
50
+ export function getRequiredMaterialLegalDocuments(): RequiredLegalDocument[] {
51
+ return Object.values(legalDocuments)
52
+ .filter((document) => document.requiresReacceptance)
53
+ .map(toRequiredLegalDocument);
54
+ }
55
+
56
+ export const memoryLegalAcceptanceAdapter =
57
+ sharedMemoryLegalAcceptanceAdapter as LegalAcceptanceAdapter;
58
+
59
+ /** @deprecated Use memoryLegalAcceptanceAdapter. Kept as a compatible alias. */
60
+ export const localLegalAcceptanceAdapter = memoryLegalAcceptanceAdapter;
61
+
62
+ export function configureLegalAcceptanceAdapter(adapter: LegalAcceptanceAdapter): void {
63
+ configureSharedLegalAcceptanceAdapter(adapter);
64
+ }
65
+
66
+ export { setLegalAcceptanceUserId, subscribeToLegalAcceptanceChanges };
67
+
68
+ export function useLegalUpdateGateSnapshot(
69
+ adapter: LegalAcceptanceAdapter = getLegalAcceptanceAdapter() as LegalAcceptanceAdapter,
70
+ userId?: string,
71
+ ) {
72
+ const requiredMaterialDocuments = useMemo(() => getRequiredMaterialLegalDocuments(), []);
73
+ const [snapshot, setSnapshot] = useState<LegalAcceptanceSnapshot>({
74
+ status: 'checking',
75
+ requiredDocuments: requiredMaterialDocuments,
76
+ acceptedDocumentKeys: [],
77
+ });
78
+ const [savingDocumentId, setSavingDocumentId] = useState<LegalDocumentId | null>(null);
79
+
80
+ const refresh = useCallback(async () => {
81
+ setSnapshot((current) => ({
82
+ ...current,
83
+ status: 'checking',
84
+ error: undefined,
85
+ }));
86
+ try {
87
+ const effectiveUserId = userId ?? getLegalAcceptanceUserId();
88
+ const nextSnapshot = await adapter.loadRequiredLegalAcceptances(
89
+ requiredMaterialDocuments,
90
+ effectiveUserId,
91
+ );
92
+ setSnapshot(nextSnapshot);
93
+ return nextSnapshot;
94
+ } catch (error) {
95
+ const message = error instanceof Error ? error.message : 'Unable to check legal acceptance.';
96
+ const errorSnapshot: LegalAcceptanceSnapshot = {
97
+ status: 'error',
98
+ requiredDocuments: requiredMaterialDocuments,
99
+ acceptedDocumentKeys: [],
100
+ error: message,
101
+ };
102
+ setSnapshot(errorSnapshot);
103
+ return errorSnapshot;
104
+ }
105
+ }, [adapter, userId, requiredMaterialDocuments]);
106
+
107
+ const acceptDocument = useCallback(
108
+ async (
109
+ document: RequiredLegalDocument,
110
+ input?: { userId?: string; flowId?: string; flowVersion?: number },
111
+ ) => {
112
+ setSavingDocumentId(document.documentId);
113
+ try {
114
+ const effectiveUserId = userId ?? getLegalAcceptanceUserId();
115
+ await adapter.acceptLegalDocument(document, {
116
+ userId: input?.userId ?? effectiveUserId,
117
+ flowId: input?.flowId,
118
+ flowVersion: input?.flowVersion,
119
+ });
120
+ if (adapter !== memoryLegalAcceptanceAdapter) {
121
+ notifyLegalAcceptanceChanged();
122
+ }
123
+ return await refresh();
124
+ } finally {
125
+ setSavingDocumentId(null);
126
+ }
127
+ },
128
+ [adapter, userId, refresh],
129
+ );
130
+
131
+ useEffect(() => {
132
+ const timer = setTimeout(() => {
133
+ void refresh();
134
+ }, 0);
135
+ return () => clearTimeout(timer);
136
+ }, [refresh]);
137
+
138
+ useEffect(
139
+ () =>
140
+ subscribeToLegalAcceptanceChanges(() => {
141
+ void refresh();
142
+ }),
143
+ [refresh],
144
+ );
145
+
146
+ return {
147
+ snapshot,
148
+ refresh,
149
+ acceptDocument,
150
+ savingDocumentId,
151
+ };
152
+ }
153
+
154
+ export function useLegalUpdateGateStatus(
155
+ adapter: LegalAcceptanceAdapter = getLegalAcceptanceAdapter() as LegalAcceptanceAdapter,
156
+ userId?: string,
157
+ ): LegalGateStatus {
158
+ return useLegalUpdateGateSnapshot(adapter, userId).snapshot.status;
159
+ }
160
+
161
+ export function useLegalAcceptance(
162
+ adapter: LegalAcceptanceAdapter = getLegalAcceptanceAdapter() as LegalAcceptanceAdapter,
163
+ userId?: string,
164
+ ) {
165
+ const requiredDocuments = useMemo(() => getRequiredMaterialLegalDocuments(), []);
166
+ const { snapshot, acceptDocument, refresh } = useLegalUpdateGateSnapshot(adapter, userId);
167
+
168
+ const acceptedDocuments = {
169
+ terms: !snapshot.requiredDocuments.some((document) => document.documentId === 'terms'),
170
+ privacy: !snapshot.requiredDocuments.some((document) => document.documentId === 'privacy'),
171
+ };
172
+
173
+ return {
174
+ acceptedDocuments,
175
+ hasAcceptedRequiredDocuments: snapshot.status === 'complete',
176
+ acceptDocument: (documentId: LegalDocumentId) => {
177
+ const document = requiredDocuments.find((item) => item.documentId === documentId);
178
+ if (document) {
179
+ return acceptDocument(document, { userId });
180
+ }
181
+ return refresh();
182
+ },
183
+ revokeDocument: () => {
184
+ throw new Error('Legal acceptance records are append-only. Replace the adapter for test resets.');
185
+ },
186
+ resetLegalAcceptance: () => {
187
+ throw new Error('Legal acceptance records are append-only. Replace the adapter for test resets.');
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,79 @@
1
+ export type LegalAcceptanceConfigAdapter = {
2
+ loadRequiredLegalAcceptances(
3
+ requiredDocuments: Array<{ documentId: string; acceptanceVersion: string }>,
4
+ userId?: string,
5
+ ): Promise<{
6
+ status: 'checking' | 'needs-legal' | 'complete' | 'error';
7
+ requiredDocuments: Array<{ documentId: string; acceptanceVersion: string }>;
8
+ acceptedDocumentKeys: string[];
9
+ error?: string;
10
+ }>;
11
+ acceptLegalDocument(
12
+ document: { documentId: string; acceptanceVersion: string },
13
+ input?: { userId?: string; flowId?: string; flowVersion?: number },
14
+ ): Promise<void>;
15
+ };
16
+
17
+ const memoryAcceptedKeys = new Set<string>();
18
+ const legalAcceptanceListeners = new Set<() => void>();
19
+ let currentLegalAcceptanceUserId: string | undefined;
20
+
21
+ function legalDocumentKey(document: { documentId: string; acceptanceVersion: string }): string {
22
+ return `${document.documentId}@${document.acceptanceVersion}`;
23
+ }
24
+
25
+ export function notifyLegalAcceptanceChanged(): void {
26
+ for (const listener of legalAcceptanceListeners) {
27
+ listener();
28
+ }
29
+ }
30
+
31
+ export function subscribeToLegalAcceptanceChanges(listener: () => void): () => void {
32
+ legalAcceptanceListeners.add(listener);
33
+ return () => {
34
+ legalAcceptanceListeners.delete(listener);
35
+ };
36
+ }
37
+
38
+ export function setLegalAcceptanceUserId(userId?: string | null): void {
39
+ const nextUserId = userId ?? undefined;
40
+ if (currentLegalAcceptanceUserId === nextUserId) {
41
+ return;
42
+ }
43
+ currentLegalAcceptanceUserId = nextUserId;
44
+ notifyLegalAcceptanceChanged();
45
+ }
46
+
47
+ export function getLegalAcceptanceUserId(): string | undefined {
48
+ return currentLegalAcceptanceUserId;
49
+ }
50
+
51
+ export const memoryLegalAcceptanceAdapter: LegalAcceptanceConfigAdapter = {
52
+ async loadRequiredLegalAcceptances(requiredDocuments) {
53
+ const missingDocuments = requiredDocuments.filter(
54
+ (document) => !memoryAcceptedKeys.has(legalDocumentKey(document)),
55
+ );
56
+
57
+ return {
58
+ status: missingDocuments.length > 0 ? 'needs-legal' : 'complete',
59
+ requiredDocuments: missingDocuments,
60
+ acceptedDocumentKeys: [...memoryAcceptedKeys],
61
+ };
62
+ },
63
+
64
+ async acceptLegalDocument(document) {
65
+ memoryAcceptedKeys.add(legalDocumentKey(document));
66
+ notifyLegalAcceptanceChanged();
67
+ },
68
+ };
69
+
70
+ let legalAcceptanceAdapter: LegalAcceptanceConfigAdapter = memoryLegalAcceptanceAdapter;
71
+
72
+ export function configureLegalAcceptanceAdapter(adapter: LegalAcceptanceConfigAdapter): void {
73
+ legalAcceptanceAdapter = adapter;
74
+ notifyLegalAcceptanceChanged();
75
+ }
76
+
77
+ export function getLegalAcceptanceAdapter(): LegalAcceptanceConfigAdapter {
78
+ return legalAcceptanceAdapter;
79
+ }
@@ -1,6 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { Pressable, StyleSheet, Text, View } from 'react-native';
3
3
 
4
+ import { getReadableTextColor } from '../../theme/color-utils';
4
5
  import { useAppTheme } from '../../theme/provider';
5
6
  import { LegalDocumentModal } from './legal-document-modal';
6
7
  import { getLegalDocument, type LegalDocumentId } from './legal-documents';
@@ -23,6 +24,8 @@ function AgreementRow({
23
24
  const theme = useAppTheme();
24
25
  const colors = theme.activeColors;
25
26
  const document = getLegalDocument(documentId);
27
+ const actionColor = accepted ? colors.success : colors.primary;
28
+ const actionForeground = getReadableTextColor(actionColor, theme.colors.light.text);
26
29
 
27
30
  return (
28
31
  <View
@@ -46,11 +49,13 @@ function AgreementRow({
46
49
  style={[
47
50
  styles.reviewButton,
48
51
  {
49
- backgroundColor: accepted ? colors.success : colors.primary,
52
+ backgroundColor: actionColor,
50
53
  borderRadius: Math.max(8, theme.layout.radius - 4),
51
54
  },
52
55
  ]}>
53
- <Text style={styles.reviewButtonText}>{accepted ? 'Accepted' : 'Review'}</Text>
56
+ <Text style={[styles.reviewButtonText, { color: actionForeground }]}>
57
+ {accepted ? 'Accepted' : 'Review'}
58
+ </Text>
54
59
  </Pressable>
55
60
  </View>
56
61
  );
@@ -62,6 +67,7 @@ export function LegalAgreementScreen({
62
67
  }: LegalAgreementScreenProps) {
63
68
  const theme = useAppTheme();
64
69
  const colors = theme.activeColors;
70
+ const primaryForeground = getReadableTextColor(colors.primary, theme.colors.light.text);
65
71
  const [activeDocument, setActiveDocument] = useState<LegalDocumentId | null>(null);
66
72
  const {
67
73
  acceptedDocuments,
@@ -114,7 +120,7 @@ export function LegalAgreementScreen({
114
120
  <Text
115
121
  style={[
116
122
  styles.continueButtonText,
117
- { color: canContinue ? '#ffffff' : colors.text },
123
+ { color: canContinue ? primaryForeground : colors.text },
118
124
  ]}>
119
125
  {continueLabel}
120
126
  </Text>
@@ -182,7 +188,6 @@ const styles = StyleSheet.create({
182
188
  paddingVertical: 10,
183
189
  },
184
190
  reviewButtonText: {
185
- color: '#ffffff',
186
191
  fontSize: 13,
187
192
  fontWeight: '900',
188
193
  },
@@ -1,5 +1,7 @@
1
- import { Modal, Pressable, SafeAreaView, StyleSheet, Text, View } from 'react-native';
1
+ import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
2
+ import { SafeAreaView } from 'react-native-safe-area-context';
2
3
 
4
+ import { getReadableTextColor } from '../../theme/color-utils';
3
5
  import { useAppTheme } from '../../theme/provider';
4
6
  import { LegalDocumentView } from './legal-document-view';
5
7
  import { getLegalDocument, type LegalDocumentId } from './legal-documents';
@@ -22,6 +24,7 @@ export function LegalDocumentModal({
22
24
  const theme = useAppTheme();
23
25
  const colors = theme.activeColors;
24
26
  const document = getLegalDocument(documentId);
27
+ const primaryForeground = getReadableTextColor(colors.primary, theme.colors.light.text);
25
28
 
26
29
  return (
27
30
  <Modal animationType="slide" presentationStyle="pageSheet" visible={visible}>
@@ -59,7 +62,9 @@ export function LegalDocumentModal({
59
62
  borderRadius: theme.layout.radius,
60
63
  },
61
64
  ]}>
62
- <Text style={styles.primaryButtonText}>{primaryActionLabel}</Text>
65
+ <Text style={[styles.primaryButtonText, { color: primaryForeground }]}>
66
+ {primaryActionLabel}
67
+ </Text>
63
68
  </Pressable>
64
69
  </View>
65
70
  </SafeAreaView>
@@ -110,7 +115,6 @@ const styles = StyleSheet.create({
110
115
  paddingVertical: 14,
111
116
  },
112
117
  primaryButtonText: {
113
- color: '#ffffff',
114
118
  fontSize: 15,
115
119
  fontWeight: '900',
116
120
  },
@@ -116,6 +116,7 @@ export function LegalDocumentView({
116
116
  <View style={styles.metaRow}>
117
117
  <LegalDocumentMeta label="Effective" value={document.effectiveDate} />
118
118
  <LegalDocumentMeta label="Last updated" value={document.lastUpdated} />
119
+ <LegalDocumentMeta label="Acceptance version" value={document.acceptanceVersion} />
119
120
  </View>
120
121
  </View>
121
122
  ) : null}
@@ -12,6 +12,9 @@ export interface LegalDocument {
12
12
  summary: string;
13
13
  effectiveDate: string;
14
14
  lastUpdated: string;
15
+ acceptanceVersion: string;
16
+ requiresReacceptance: boolean;
17
+ changeSummary: string;
15
18
  sections: LegalDocumentSection[];
16
19
  }
17
20
 
@@ -26,6 +29,10 @@ export const legalDocuments: Record<LegalDocumentId, LegalDocument> = {
26
29
  'Starter terms for __MDS_APP_NAME__ with common sections your legal reviewer can replace.',
27
30
  effectiveDate: '2026-08-10',
28
31
  lastUpdated: '2026-08-10',
32
+ acceptanceVersion: '2026-08-10',
33
+ requiresReacceptance: true,
34
+ changeSummary:
35
+ 'Initial material Terms of Service version that should be accepted before protected app access.',
29
36
  sections: [
30
37
  {
31
38
  id: 'acceptance',
@@ -72,7 +79,8 @@ export const legalDocuments: Record<LegalDocumentId, LegalDocument> = {
72
79
  title: 'Changes and Contact',
73
80
  body: [
74
81
  'These terms may be updated as product functionality, business requirements, or legal obligations change.',
75
- 'Add your support email, business address, and jurisdiction-specific notice requirements here.',
82
+ '__MDS_LEGAL_BUSINESS_NAME__ is the business responsible for these terms. Direct legal questions to __MDS_LEGAL_CONTACT_EMAIL__ and replace this line with your reviewed notice workflow.',
83
+ 'Replace this placeholder with your business address, jurisdiction, and local notice requirements: __MDS_LEGAL_ADDRESS_OR_REGION_NOTE__.',
76
84
  ],
77
85
  },
78
86
  ],
@@ -84,6 +92,10 @@ export const legalDocuments: Record<LegalDocumentId, LegalDocument> = {
84
92
  'Starter privacy policy for __MDS_APP_NAME__ with common data and rights sections to replace.',
85
93
  effectiveDate: '2026-08-10',
86
94
  lastUpdated: '2026-08-10',
95
+ acceptanceVersion: '2026-08-10',
96
+ requiresReacceptance: true,
97
+ changeSummary:
98
+ 'Initial material Privacy Policy version that should be accepted before protected app access.',
87
99
  sections: [
88
100
  {
89
101
  id: 'data-processed',
@@ -121,8 +133,10 @@ export const legalDocuments: Record<LegalDocumentId, LegalDocument> = {
121
133
  id: 'rights-contact',
122
134
  title: 'Your Choices and Contact',
123
135
  body: [
124
- 'Users may have rights to access, correct, delete, export, or object to processing of their data depending on jurisdiction.',
125
- 'Add the real contact channel and request workflow for privacy questions before production use.',
136
+ 'Users may have rights to access, correct, delete, export, restrict, or object to processing depending on jurisdiction, including GDPR and comparable privacy laws.',
137
+ 'For GDPR-facing flows, document your lawful bases, cross-border transfer process, retention rules, and response timelines for data-subject requests.',
138
+ 'Route privacy requests to __MDS_LEGAL_CONTACT_EMAIL__ on behalf of __MDS_LEGAL_BUSINESS_NAME__, and replace this placeholder workflow before production use.',
139
+ 'Replace this location or jurisdiction note with the reviewed business address, EU representative, or regional contact process that applies: __MDS_LEGAL_ADDRESS_OR_REGION_NOTE__.',
126
140
  ],
127
141
  },
128
142
  ],