@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.
- package/README.md +3 -0
- package/assets/mds/env/convex.env.example +1 -0
- package/assets/mds/env/firebase.env.example +6 -0
- package/assets/mds/env/supabase.env.example +3 -0
- package/assets/mds/project/auth-base.md +17 -0
- package/assets/mds/project/auth-convex.md +33 -0
- package/assets/mds/project/auth-firebase.md +22 -0
- package/assets/mds/project/auth-supabase.md +46 -0
- package/assets/mds/src/app/(auth)/reset-password.tsx +5 -0
- package/assets/mds/src/app/(auth)/sign-in.tsx +5 -0
- package/assets/mds/src/app/(auth)/sign-up.tsx +5 -0
- package/assets/mds/src/app/legal/updates.tsx +1 -0
- package/assets/mds/src/app/onboarding/complete.tsx +2 -0
- package/assets/mds/src/app/onboarding/features.tsx +1 -0
- package/assets/mds/src/app/onboarding/legal.tsx +2 -0
- package/assets/mds/src/app/onboarding.tsx +1 -1
- package/assets/mds/src/app/settings.tsx +10 -1
- package/assets/mds/src/db/adapter.ts +195 -0
- package/assets/mds/src/db/firebase.ts +77 -0
- package/assets/mds/src/db/index.firebase.ts +19 -0
- package/assets/mds/src/db/index.supabase.ts +22 -0
- package/assets/mds/src/db/supabase.ts +337 -0
- package/assets/mds/src/features/auth/adapters/base-auth-adapter.tsx +82 -0
- package/assets/mds/src/features/auth/adapters/convex-auth-adapter.tsx +156 -0
- package/assets/mds/src/features/auth/adapters/firebase-auth-adapter.tsx +147 -0
- package/assets/mds/src/features/auth/adapters/supabase-auth-adapter.tsx +166 -0
- package/assets/mds/src/features/auth/auth-guard-logic.ts +19 -0
- package/assets/mds/src/features/auth/auth-guard.tsx +77 -0
- package/assets/mds/src/features/auth/auth-provider.tsx +60 -0
- package/assets/mds/src/features/auth/auth-screen.tsx +265 -0
- package/assets/mds/src/features/auth/auth-types.ts +45 -0
- package/assets/mds/src/features/exposition/data-screen.tsx +4 -2
- package/assets/mds/src/features/exposition/expo-sdk-56-screen.tsx +119 -76
- package/assets/mds/src/features/exposition/exposition-screen.tsx +10 -11
- package/assets/mds/src/features/exposition/stylist-screen.tsx +62 -11
- package/assets/mds/src/features/legal/legal-acceptance-adapter.ts +190 -0
- package/assets/mds/src/features/legal/legal-acceptance-config.ts +79 -0
- package/assets/mds/src/features/legal/legal-agreement-screen.tsx +9 -4
- package/assets/mds/src/features/legal/legal-document-modal.tsx +7 -3
- package/assets/mds/src/features/legal/legal-document-view.tsx +1 -0
- package/assets/mds/src/features/legal/legal-documents.ts +17 -3
- package/assets/mds/src/features/legal/legal-update-screen.tsx +297 -0
- package/assets/mds/src/features/legal/use-legal-acceptance.ts +1 -38
- package/assets/mds/src/features/onboarding/complete-screen.tsx +138 -0
- package/assets/mds/src/features/onboarding/features-screen.tsx +129 -0
- package/assets/mds/src/features/onboarding/legal-review-screen.tsx +242 -0
- package/assets/mds/src/features/onboarding/onboarding-config-with-legal.ts +104 -0
- package/assets/mds/src/features/onboarding/onboarding-config.ts +104 -0
- package/assets/mds/src/features/onboarding/onboarding-persistence-sync.tsx +1 -0
- package/assets/mds/src/features/onboarding/welcome-screen.tsx +130 -0
- package/assets/mds/src/features/onboarding-state/adapters/memory-onboarding-state-adapter.ts +15 -0
- package/assets/mds/src/features/onboarding-state/adapters/supabase-onboarding-state-adapter.ts +45 -0
- package/assets/mds/src/features/onboarding-state/adapters/zustand-onboarding-state-adapter.ts +16 -0
- package/assets/mds/src/features/onboarding-state/adapters/zustand-supabase-onboarding-state-adapter.ts +53 -0
- package/assets/mds/src/features/onboarding-state/onboarding-state-core.ts +108 -0
- package/assets/mds/src/features/onboarding-state/onboarding-state-memory.ts +106 -0
- package/assets/mds/src/features/onboarding-state/onboarding-state-supabase.ts +246 -0
- package/assets/mds/src/features/onboarding-state/onboarding-state-types.ts +64 -0
- package/assets/mds/src/features/onboarding-state/onboarding-state-zustand-supabase.ts +120 -0
- package/assets/mds/src/features/onboarding-state/onboarding-state-zustand.ts +71 -0
- package/assets/mds/src/features/onboarding-state/onboarding-state.ts +22 -0
- package/assets/mds/src/features/onboarding-state/onboarding-store.ts +39 -0
- package/assets/mds/src/features/settings/settings-screen-logic.ts +122 -0
- package/assets/mds/src/features/settings/settings-screen.tsx +274 -28
- package/assets/mds/src/services/convex.ts +40 -0
- package/assets/mds/src/services/firebase.ts +70 -0
- package/assets/mds/src/services/supabase.ts +50 -0
- package/assets/mds/src/theme/color-utils.ts +42 -0
- package/assets/mds/src/theme/provider.tsx +15 -5
- package/assets/mds/src/types/database.ts +13 -0
- package/assets/mds/supabase/migrations/0001_mds_auth_onboarding.sql +107 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +444 -29
- package/dist/catalog.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/assets/mds/src/app/onboarding/account-setup.tsx +0 -1
- package/assets/mds/src/app/onboarding/agreement.tsx +0 -1
- package/assets/mds/src/app/onboarding/terms.tsx +0 -1
- package/assets/mds/src/features/onboarding/account-setup-screen.tsx +0 -54
- package/assets/mds/src/features/onboarding/agreement-screen.tsx +0 -6
- package/assets/mds/src/features/onboarding/components/legal-document-view.tsx +0 -103
- package/assets/mds/src/features/onboarding/legal-documents.ts +0 -80
- package/assets/mds/src/features/onboarding/onboarding-screen.tsx +0 -167
- 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
|
|
1035
|
+
const hydratedTheme = withFontFamilyAlias(
|
|
1011
1036
|
normalizeThemeTypography(
|
|
1012
1037
|
reconcileTheme(payload.theme, 'picker', defaultBgFamilies, defaultBgFamilyShades)
|
|
1013
1038
|
)
|
|
1014
1039
|
);
|
|
1015
|
-
|
|
1016
|
-
|
|
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:
|
|
52
|
+
backgroundColor: actionColor,
|
|
50
53
|
borderRadius: Math.max(8, theme.layout.radius - 4),
|
|
51
54
|
},
|
|
52
55
|
]}>
|
|
53
|
-
<Text style={styles.reviewButtonText
|
|
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 ?
|
|
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,
|
|
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
|
|
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
|
-
'
|
|
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
|
|
125
|
-
'
|
|
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
|
],
|