@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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEmptyOnboardingState,
|
|
3
|
+
upsertLegalAcceptance,
|
|
4
|
+
type OnboardingState,
|
|
5
|
+
type OnboardingStateAdapter,
|
|
6
|
+
} from './onboarding-state-types';
|
|
7
|
+
import {
|
|
8
|
+
getPersistedOnboardingState,
|
|
9
|
+
setPersistedOnboardingState,
|
|
10
|
+
} from './onboarding-store';
|
|
11
|
+
import type { MemoryLegalAcceptanceAdapter, MemoryLegalDocument } from './onboarding-state-memory';
|
|
12
|
+
|
|
13
|
+
export function createZustandOnboardingStateAdapter(): OnboardingStateAdapter {
|
|
14
|
+
return {
|
|
15
|
+
mode: 'zustand-local',
|
|
16
|
+
async loadState() {
|
|
17
|
+
const state = getPersistedOnboardingState();
|
|
18
|
+
return { ...state, legalAcceptances: [...state.legalAcceptances] };
|
|
19
|
+
},
|
|
20
|
+
async saveState(state: OnboardingState) {
|
|
21
|
+
setPersistedOnboardingState({
|
|
22
|
+
...state,
|
|
23
|
+
legalAcceptances: [...state.legalAcceptances],
|
|
24
|
+
pendingSync: false,
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
async markComplete(input) {
|
|
28
|
+
const current = getPersistedOnboardingState();
|
|
29
|
+
const next = {
|
|
30
|
+
...current,
|
|
31
|
+
completedAt: input?.completedAt ?? new Date().toISOString(),
|
|
32
|
+
currentStep: 'complete',
|
|
33
|
+
pendingSync: false,
|
|
34
|
+
};
|
|
35
|
+
setPersistedOnboardingState(next);
|
|
36
|
+
return next;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createZustandLegalAcceptanceAdapter(): MemoryLegalAcceptanceAdapter {
|
|
42
|
+
return {
|
|
43
|
+
async loadRequiredLegalAcceptances(requiredDocuments: MemoryLegalDocument[]) {
|
|
44
|
+
const acceptedDocumentKeys = getPersistedOnboardingState().legalAcceptances.map(
|
|
45
|
+
(item) => `${item.documentId}@${item.documentVersion}`,
|
|
46
|
+
);
|
|
47
|
+
const accepted = new Set(acceptedDocumentKeys);
|
|
48
|
+
const missingDocuments = requiredDocuments.filter(
|
|
49
|
+
(document) => !accepted.has(`${document.documentId}@${document.acceptanceVersion}`),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
status: missingDocuments.length > 0 ? 'needs-legal' : 'complete',
|
|
54
|
+
requiredDocuments: missingDocuments,
|
|
55
|
+
acceptedDocumentKeys,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async acceptLegalDocument(document, input) {
|
|
60
|
+
const next = upsertLegalAcceptance(getPersistedOnboardingState() ?? createEmptyOnboardingState(), {
|
|
61
|
+
documentId: document.documentId,
|
|
62
|
+
documentVersion: document.acceptanceVersion,
|
|
63
|
+
acceptedAt: new Date().toISOString(),
|
|
64
|
+
userId: input?.userId,
|
|
65
|
+
flowId: input?.flowId,
|
|
66
|
+
flowVersion: input?.flowVersion,
|
|
67
|
+
});
|
|
68
|
+
setPersistedOnboardingState(next);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import './onboarding-state-adapter';
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
configureOnboardingStateAdapter,
|
|
5
|
+
getOnboardingStateAdapter,
|
|
6
|
+
markOnboardingComplete,
|
|
7
|
+
setOnboardingStateUserId,
|
|
8
|
+
subscribeToOnboardingStateChanges,
|
|
9
|
+
useOnboardingState,
|
|
10
|
+
} from './onboarding-state-core';
|
|
11
|
+
export {
|
|
12
|
+
DEFAULT_ONBOARDING_FLOW_ID,
|
|
13
|
+
DEFAULT_ONBOARDING_FLOW_VERSION,
|
|
14
|
+
createEmptyOnboardingState,
|
|
15
|
+
upsertLegalAcceptance,
|
|
16
|
+
} from './onboarding-state-types';
|
|
17
|
+
export type {
|
|
18
|
+
OnboardingDocumentAcceptance,
|
|
19
|
+
OnboardingPersistenceMode,
|
|
20
|
+
OnboardingState,
|
|
21
|
+
OnboardingStateAdapter,
|
|
22
|
+
} from './onboarding-state-types';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createEmptyOnboardingState,
|
|
7
|
+
type OnboardingState,
|
|
8
|
+
} from './onboarding-state-types';
|
|
9
|
+
|
|
10
|
+
type OnboardingStore = {
|
|
11
|
+
state: OnboardingState;
|
|
12
|
+
setOnboardingState: (state: OnboardingState) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const useOnboardingStore = create<OnboardingStore>()(
|
|
16
|
+
persist(
|
|
17
|
+
(set) => ({
|
|
18
|
+
state: createEmptyOnboardingState(),
|
|
19
|
+
setOnboardingState: (state) => set({ state }),
|
|
20
|
+
}),
|
|
21
|
+
{
|
|
22
|
+
name: 'mds.onboarding.state.v1',
|
|
23
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
24
|
+
partialize: (value) => ({ state: value.state }),
|
|
25
|
+
},
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export function getPersistedOnboardingState(): OnboardingState {
|
|
30
|
+
return useOnboardingStore.getState().state;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setPersistedOnboardingState(state: OnboardingState): void {
|
|
34
|
+
useOnboardingStore.getState().setOnboardingState(state);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function subscribeToPersistedOnboardingState(listener: () => void): () => void {
|
|
38
|
+
return useOnboardingStore.subscribe(listener);
|
|
39
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { AuthAdapter } from '../auth/auth-types';
|
|
2
|
+
|
|
3
|
+
export interface SettingsLegalUrls {
|
|
4
|
+
privacy?: string;
|
|
5
|
+
terms?: string;
|
|
6
|
+
gdpr?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SettingsVersionInfo {
|
|
10
|
+
version: string;
|
|
11
|
+
build?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SettingsLinkItem {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
href: string;
|
|
18
|
+
description: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SettingsConstantsShape {
|
|
22
|
+
expoConfig?: {
|
|
23
|
+
version?: string;
|
|
24
|
+
ios?: { buildNumber?: string | null };
|
|
25
|
+
android?: { versionCode?: number | null };
|
|
26
|
+
} | null;
|
|
27
|
+
nativeAppVersion?: string | null;
|
|
28
|
+
nativeBuildVersion?: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveSettingsVersionInfo(
|
|
32
|
+
source: SettingsConstantsShape
|
|
33
|
+
): SettingsVersionInfo {
|
|
34
|
+
const version =
|
|
35
|
+
source.expoConfig?.version ?? source.nativeAppVersion ?? 'Development build';
|
|
36
|
+
const build =
|
|
37
|
+
source.nativeBuildVersion ??
|
|
38
|
+
source.expoConfig?.ios?.buildNumber ??
|
|
39
|
+
(typeof source.expoConfig?.android?.versionCode === 'number'
|
|
40
|
+
? String(source.expoConfig.android.versionCode)
|
|
41
|
+
: undefined);
|
|
42
|
+
|
|
43
|
+
return { version, ...(build ? { build } : {}) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildSettingsLinkItems(
|
|
47
|
+
legalUrls?: SettingsLegalUrls,
|
|
48
|
+
profileHref?: string
|
|
49
|
+
): SettingsLinkItem[] {
|
|
50
|
+
return [
|
|
51
|
+
...(profileHref
|
|
52
|
+
? [
|
|
53
|
+
{
|
|
54
|
+
id: 'profile',
|
|
55
|
+
label: 'Manage profile',
|
|
56
|
+
href: profileHref,
|
|
57
|
+
description: 'Open the account or profile surface for this app.',
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
: []),
|
|
61
|
+
...(legalUrls?.terms
|
|
62
|
+
? [
|
|
63
|
+
{
|
|
64
|
+
id: 'terms',
|
|
65
|
+
label: 'Terms of Service',
|
|
66
|
+
href: legalUrls.terms,
|
|
67
|
+
description: 'Review service usage, account, and policy terms.',
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
: []),
|
|
71
|
+
...(legalUrls?.privacy
|
|
72
|
+
? [
|
|
73
|
+
{
|
|
74
|
+
id: 'privacy',
|
|
75
|
+
label: 'Privacy Policy',
|
|
76
|
+
href: legalUrls.privacy,
|
|
77
|
+
description: 'Review data processing, retention, and user choices.',
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
: []),
|
|
81
|
+
...(legalUrls?.gdpr
|
|
82
|
+
? [
|
|
83
|
+
{
|
|
84
|
+
id: 'gdpr',
|
|
85
|
+
label: 'GDPR Notice',
|
|
86
|
+
href: legalUrls.gdpr,
|
|
87
|
+
description: 'Review data-subject rights, exports, and deletion requests.',
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
: []),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createPlaceholderAuthAdapter(
|
|
95
|
+
provider: AuthAdapter['provider'] = 'base'
|
|
96
|
+
): AuthAdapter {
|
|
97
|
+
return {
|
|
98
|
+
provider,
|
|
99
|
+
state: { isLoading: false, session: null },
|
|
100
|
+
refreshSession: async () => {},
|
|
101
|
+
signInWithEmailPassword: async () => ({
|
|
102
|
+
ok: false,
|
|
103
|
+
error: 'Install and wire the auth library before using sign-in from Settings.',
|
|
104
|
+
}),
|
|
105
|
+
signUpWithEmailPassword: async () => ({
|
|
106
|
+
ok: false,
|
|
107
|
+
error: 'Install and wire the auth library before using sign-up from Settings.',
|
|
108
|
+
}),
|
|
109
|
+
requestPasswordReset: async () => ({
|
|
110
|
+
ok: false,
|
|
111
|
+
error: 'Install and wire the auth library before using password reset from Settings.',
|
|
112
|
+
}),
|
|
113
|
+
signOut: async () => ({
|
|
114
|
+
ok: true,
|
|
115
|
+
message: 'No auth provider is configured for this generated Settings screen yet.',
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function performSettingsSignOut(auth: AuthAdapter) {
|
|
121
|
+
return auth.signOut();
|
|
122
|
+
}
|
|
@@ -1,57 +1,303 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Href } from 'expo-router';
|
|
2
|
+
import { useRouter } from 'expo-router';
|
|
3
|
+
import Constants from 'expo-constants';
|
|
4
|
+
import { useMemo, useState } from 'react';
|
|
5
|
+
import { Linking, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
2
6
|
|
|
3
|
-
import {
|
|
7
|
+
import { getReadableTextColor } from '../../theme/color-utils';
|
|
4
8
|
import { useAppTheme } from '../../theme/provider';
|
|
9
|
+
import {
|
|
10
|
+
buildSettingsLinkItems,
|
|
11
|
+
createPlaceholderAuthAdapter,
|
|
12
|
+
performSettingsSignOut,
|
|
13
|
+
resolveSettingsVersionInfo as resolveSettingsVersionInfoFromConstants,
|
|
14
|
+
type SettingsConstantsShape,
|
|
15
|
+
type SettingsLegalUrls,
|
|
16
|
+
} from './settings-screen-logic';
|
|
5
17
|
|
|
6
|
-
|
|
18
|
+
import type { AuthAdapter } from '../auth/auth-types';
|
|
19
|
+
|
|
20
|
+
export interface SettingsScreenProps {
|
|
21
|
+
auth: AuthAdapter;
|
|
22
|
+
legalUrls?: SettingsLegalUrls;
|
|
23
|
+
profileHref?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveSettingsVersionInfo(
|
|
27
|
+
source: SettingsConstantsShape = Constants as SettingsConstantsShape
|
|
28
|
+
){
|
|
29
|
+
return resolveSettingsVersionInfoFromConstants(source);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveInternalSettingsHref(href: string): Href | null {
|
|
33
|
+
if (href.startsWith('/')) {
|
|
34
|
+
return href as Href;
|
|
35
|
+
}
|
|
36
|
+
if (typeof window === 'undefined') {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(href, window.location.origin);
|
|
41
|
+
if (url.origin !== window.location.origin) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return `${url.pathname}${url.search}${url.hash}` as Href;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function openSettingsHref(href: string, push: (href: Href) => void): Promise<void> {
|
|
51
|
+
const internalHref = resolveInternalSettingsHref(href);
|
|
52
|
+
if (internalHref) {
|
|
53
|
+
push(internalHref);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await Linking.openURL(href);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default function SettingsScreen({
|
|
60
|
+
auth,
|
|
61
|
+
legalUrls,
|
|
62
|
+
profileHref,
|
|
63
|
+
}: SettingsScreenProps) {
|
|
64
|
+
const router = useRouter();
|
|
7
65
|
const theme = useAppTheme();
|
|
8
66
|
const colors = theme.activeColors;
|
|
67
|
+
const user = auth.state.session?.user ?? null;
|
|
68
|
+
const versionInfo = useMemo(() => resolveSettingsVersionInfo(), []);
|
|
69
|
+
const links = useMemo(
|
|
70
|
+
() => buildSettingsLinkItems(legalUrls, profileHref),
|
|
71
|
+
[legalUrls, profileHref]
|
|
72
|
+
);
|
|
73
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
74
|
+
const [isSigningOut, setIsSigningOut] = useState(false);
|
|
75
|
+
const dangerForeground = getReadableTextColor(colors.warning, theme.colors.light.text);
|
|
76
|
+
|
|
77
|
+
async function handleSignOut() {
|
|
78
|
+
if (isSigningOut) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
setIsSigningOut(true);
|
|
82
|
+
setNotice(null);
|
|
83
|
+
const result = await performSettingsSignOut(auth);
|
|
84
|
+
setIsSigningOut(false);
|
|
85
|
+
setNotice(result.ok ? result.message ?? 'Signed out.' : result.error);
|
|
86
|
+
}
|
|
9
87
|
|
|
10
88
|
return (
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
89
|
+
<ScrollView
|
|
90
|
+
contentInsetAdjustmentBehavior="automatic"
|
|
91
|
+
contentContainerStyle={styles.content}
|
|
92
|
+
style={[styles.screen, { backgroundColor: colors.background }]}>
|
|
93
|
+
<View
|
|
94
|
+
style={[
|
|
95
|
+
styles.panel,
|
|
96
|
+
{
|
|
97
|
+
backgroundColor: colors.surface,
|
|
98
|
+
borderColor: colors.primary,
|
|
99
|
+
borderRadius: theme.layout.radius,
|
|
100
|
+
},
|
|
101
|
+
]}>
|
|
102
|
+
<View style={styles.header}>
|
|
103
|
+
<Text
|
|
104
|
+
style={[
|
|
105
|
+
styles.title,
|
|
106
|
+
{
|
|
107
|
+
color: colors.text,
|
|
108
|
+
fontFamily: theme.typography.fontTitle,
|
|
109
|
+
},
|
|
110
|
+
]}>
|
|
111
|
+
Settings
|
|
112
|
+
</Text>
|
|
113
|
+
<Text style={[styles.body, { color: colors.text }]}>
|
|
114
|
+
Review account details, legal documents, and release information in one place.
|
|
115
|
+
</Text>
|
|
116
|
+
</View>
|
|
117
|
+
|
|
118
|
+
<View
|
|
14
119
|
style={[
|
|
15
|
-
styles.
|
|
120
|
+
styles.card,
|
|
16
121
|
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
theme.typography.fontFamily === 'System' ||
|
|
21
|
-
theme.typography.fontFamily === 'monospace'
|
|
22
|
-
? '800'
|
|
23
|
-
: 'normal',
|
|
122
|
+
backgroundColor: colors.background,
|
|
123
|
+
borderColor: colors.primary,
|
|
124
|
+
borderRadius: theme.layout.radius,
|
|
24
125
|
},
|
|
25
126
|
]}>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
127
|
+
<Text style={[styles.sectionLabel, { color: colors.primary }]}>Account</Text>
|
|
128
|
+
<Text style={[styles.accountTitle, { color: colors.text }]}>
|
|
129
|
+
{user?.name?.trim() || user?.email?.trim() || 'Signed-out preview'}
|
|
130
|
+
</Text>
|
|
131
|
+
<Text style={[styles.accountMeta, { color: colors.text }]}>
|
|
132
|
+
{user?.email?.trim() || 'No authenticated session is active.'}
|
|
133
|
+
</Text>
|
|
134
|
+
<Text style={[styles.accountMeta, { color: colors.text }]}>
|
|
135
|
+
Provider: {user?.provider || auth.provider}
|
|
136
|
+
</Text>
|
|
137
|
+
</View>
|
|
138
|
+
|
|
139
|
+
{links.length > 0 ? (
|
|
140
|
+
<View style={styles.section}>
|
|
141
|
+
<Text style={[styles.sectionLabel, { color: colors.primary }]}>Links</Text>
|
|
142
|
+
<View style={styles.linkStack}>
|
|
143
|
+
{links.map((item) => (
|
|
144
|
+
<Pressable
|
|
145
|
+
key={item.id}
|
|
146
|
+
accessibilityRole="button"
|
|
147
|
+
onPress={() => void openSettingsHref(item.href, router.push)}
|
|
148
|
+
style={[
|
|
149
|
+
styles.linkCard,
|
|
150
|
+
{
|
|
151
|
+
backgroundColor: colors.background,
|
|
152
|
+
borderColor: colors.primary,
|
|
153
|
+
borderRadius: theme.layout.radius,
|
|
154
|
+
},
|
|
155
|
+
]}>
|
|
156
|
+
<Text style={[styles.linkLabel, { color: colors.text }]}>{item.label}</Text>
|
|
157
|
+
<Text style={[styles.linkDescription, { color: colors.text }]}>
|
|
158
|
+
{item.description}
|
|
159
|
+
</Text>
|
|
160
|
+
</Pressable>
|
|
161
|
+
))}
|
|
162
|
+
</View>
|
|
163
|
+
</View>
|
|
164
|
+
) : null}
|
|
165
|
+
|
|
166
|
+
<View style={styles.section}>
|
|
167
|
+
<Text style={[styles.sectionLabel, { color: colors.primary }]}>App info</Text>
|
|
168
|
+
<View
|
|
169
|
+
style={[
|
|
170
|
+
styles.card,
|
|
171
|
+
{
|
|
172
|
+
backgroundColor: colors.background,
|
|
173
|
+
borderColor: colors.primary,
|
|
174
|
+
borderRadius: theme.layout.radius,
|
|
175
|
+
},
|
|
176
|
+
]}>
|
|
177
|
+
<Text style={[styles.accountTitle, { color: colors.text }]}>
|
|
178
|
+
Version {versionInfo.version}
|
|
179
|
+
</Text>
|
|
180
|
+
{versionInfo.build ? (
|
|
181
|
+
<Text style={[styles.accountMeta, { color: colors.text }]}>
|
|
182
|
+
Build {versionInfo.build}
|
|
183
|
+
</Text>
|
|
184
|
+
) : null}
|
|
185
|
+
</View>
|
|
186
|
+
</View>
|
|
187
|
+
|
|
188
|
+
<View style={styles.actions}>
|
|
189
|
+
<Pressable
|
|
190
|
+
accessibilityRole="button"
|
|
191
|
+
disabled={!user || isSigningOut}
|
|
192
|
+
onPress={() => void handleSignOut()}
|
|
193
|
+
style={[
|
|
194
|
+
styles.primaryButton,
|
|
195
|
+
{
|
|
196
|
+
backgroundColor: user ? colors.warning : colors.surface,
|
|
197
|
+
borderRadius: theme.layout.radius,
|
|
198
|
+
opacity: !user || isSigningOut ? 0.7 : 1,
|
|
199
|
+
},
|
|
200
|
+
]}>
|
|
201
|
+
<Text
|
|
202
|
+
style={[
|
|
203
|
+
styles.primaryButtonText,
|
|
204
|
+
{ color: user ? dangerForeground : colors.text },
|
|
205
|
+
]}>
|
|
206
|
+
{isSigningOut ? 'Signing out...' : 'Sign Out'}
|
|
207
|
+
</Text>
|
|
208
|
+
</Pressable>
|
|
209
|
+
</View>
|
|
210
|
+
|
|
211
|
+
{notice ? <Text style={[styles.notice, { color: colors.text }]}>{notice}</Text> : null}
|
|
31
212
|
</View>
|
|
32
|
-
|
|
33
|
-
</View>
|
|
213
|
+
</ScrollView>
|
|
34
214
|
);
|
|
35
215
|
}
|
|
36
216
|
|
|
37
217
|
const styles = StyleSheet.create({
|
|
38
218
|
screen: {
|
|
39
|
-
backgroundColor: '#ffffff',
|
|
40
219
|
flex: 1,
|
|
220
|
+
},
|
|
221
|
+
content: {
|
|
222
|
+
flexGrow: 1,
|
|
223
|
+
justifyContent: 'center',
|
|
41
224
|
padding: 20,
|
|
42
225
|
},
|
|
226
|
+
panel: {
|
|
227
|
+
alignSelf: 'center',
|
|
228
|
+
borderWidth: 1,
|
|
229
|
+
gap: 18,
|
|
230
|
+
maxWidth: 760,
|
|
231
|
+
padding: 18,
|
|
232
|
+
width: '100%',
|
|
233
|
+
},
|
|
43
234
|
header: {
|
|
44
|
-
|
|
235
|
+
gap: 6,
|
|
45
236
|
},
|
|
46
237
|
title: {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
238
|
+
fontSize: 30,
|
|
239
|
+
fontWeight: '900',
|
|
240
|
+
lineHeight: 36,
|
|
50
241
|
},
|
|
51
242
|
body: {
|
|
52
|
-
|
|
243
|
+
fontSize: 15,
|
|
244
|
+
lineHeight: 22,
|
|
245
|
+
},
|
|
246
|
+
section: {
|
|
247
|
+
gap: 10,
|
|
248
|
+
},
|
|
249
|
+
sectionLabel: {
|
|
250
|
+
fontSize: 12,
|
|
251
|
+
fontWeight: '900',
|
|
252
|
+
textTransform: 'uppercase',
|
|
253
|
+
},
|
|
254
|
+
card: {
|
|
255
|
+
borderWidth: 1,
|
|
256
|
+
gap: 6,
|
|
257
|
+
padding: 14,
|
|
258
|
+
},
|
|
259
|
+
accountTitle: {
|
|
260
|
+
fontSize: 18,
|
|
261
|
+
fontWeight: '900',
|
|
262
|
+
},
|
|
263
|
+
accountMeta: {
|
|
53
264
|
fontSize: 14,
|
|
54
265
|
lineHeight: 20,
|
|
55
|
-
|
|
266
|
+
},
|
|
267
|
+
linkStack: {
|
|
268
|
+
gap: 10,
|
|
269
|
+
},
|
|
270
|
+
linkCard: {
|
|
271
|
+
borderWidth: 1,
|
|
272
|
+
gap: 4,
|
|
273
|
+
padding: 14,
|
|
274
|
+
},
|
|
275
|
+
linkLabel: {
|
|
276
|
+
fontSize: 15,
|
|
277
|
+
fontWeight: '900',
|
|
278
|
+
},
|
|
279
|
+
linkDescription: {
|
|
280
|
+
fontSize: 13,
|
|
281
|
+
lineHeight: 18,
|
|
282
|
+
},
|
|
283
|
+
actions: {
|
|
284
|
+
flexDirection: 'row',
|
|
285
|
+
flexWrap: 'wrap',
|
|
286
|
+
gap: 10,
|
|
287
|
+
},
|
|
288
|
+
primaryButton: {
|
|
289
|
+
alignItems: 'center',
|
|
290
|
+
justifyContent: 'center',
|
|
291
|
+
minHeight: 48,
|
|
292
|
+
minWidth: 160,
|
|
293
|
+
paddingHorizontal: 16,
|
|
294
|
+
},
|
|
295
|
+
primaryButtonText: {
|
|
296
|
+
fontSize: 15,
|
|
297
|
+
fontWeight: '900',
|
|
298
|
+
},
|
|
299
|
+
notice: {
|
|
300
|
+
fontSize: 13,
|
|
301
|
+
lineHeight: 19,
|
|
56
302
|
},
|
|
57
303
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ConvexReactClient } from 'convex/react';
|
|
2
|
+
import * as SecureStore from 'expo-secure-store';
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
|
|
5
|
+
const convexUrl = process.env.EXPO_PUBLIC_CONVEX_URL;
|
|
6
|
+
export const isConvexConfigured = Boolean(convexUrl);
|
|
7
|
+
|
|
8
|
+
let convexClient: ConvexReactClient | null = null;
|
|
9
|
+
|
|
10
|
+
export function getConvexClient(): ConvexReactClient {
|
|
11
|
+
if (!convexUrl) {
|
|
12
|
+
throw new Error('Set EXPO_PUBLIC_CONVEX_URL before using Convex Auth.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
convexClient ??= new ConvexReactClient(convexUrl);
|
|
16
|
+
return convexClient;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const convexAuthStorage = {
|
|
20
|
+
async getItem(key: string) {
|
|
21
|
+
if (Platform.OS === 'web') {
|
|
22
|
+
return globalThis.localStorage?.getItem(key) ?? null;
|
|
23
|
+
}
|
|
24
|
+
return SecureStore.getItemAsync(key);
|
|
25
|
+
},
|
|
26
|
+
async setItem(key: string, value: string) {
|
|
27
|
+
if (Platform.OS === 'web') {
|
|
28
|
+
globalThis.localStorage?.setItem(key, value);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await SecureStore.setItemAsync(key, value);
|
|
32
|
+
},
|
|
33
|
+
async removeItem(key: string) {
|
|
34
|
+
if (Platform.OS === 'web') {
|
|
35
|
+
globalThis.localStorage?.removeItem(key);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await SecureStore.deleteItemAsync(key);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import { getApp, getApps, initializeApp } from 'firebase/app';
|
|
3
|
+
import { getAuth, getReactNativePersistence, initializeAuth } from 'firebase/auth';
|
|
4
|
+
import { getFirestore } from 'firebase/firestore';
|
|
5
|
+
|
|
6
|
+
import type { FirebaseApp } from 'firebase/app';
|
|
7
|
+
import type { Auth } from 'firebase/auth';
|
|
8
|
+
import type { Firestore } from 'firebase/firestore';
|
|
9
|
+
|
|
10
|
+
const firebaseConfig = {
|
|
11
|
+
apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
|
|
12
|
+
authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
|
13
|
+
projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID,
|
|
14
|
+
storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
|
15
|
+
messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
|
16
|
+
appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const isFirebaseConfigured = Boolean(
|
|
20
|
+
firebaseConfig.apiKey &&
|
|
21
|
+
firebaseConfig.authDomain &&
|
|
22
|
+
firebaseConfig.projectId &&
|
|
23
|
+
firebaseConfig.appId,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const firebaseApp: FirebaseApp | null = isFirebaseConfigured
|
|
27
|
+
? getApps().length > 0
|
|
28
|
+
? getApp()
|
|
29
|
+
: initializeApp(firebaseConfig)
|
|
30
|
+
: null;
|
|
31
|
+
|
|
32
|
+
function createFirebaseAuth(): Auth {
|
|
33
|
+
if (!firebaseApp) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'Set EXPO_PUBLIC_FIREBASE_API_KEY, EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN, EXPO_PUBLIC_FIREBASE_PROJECT_ID, and EXPO_PUBLIC_FIREBASE_APP_ID before using Firebase.',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return initializeAuth(firebaseApp, {
|
|
40
|
+
persistence: getReactNativePersistence(AsyncStorage),
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return getAuth(firebaseApp);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const firebaseAuth = firebaseApp ? createFirebaseAuth() : null;
|
|
48
|
+
export const firebaseDb: Firestore | null = firebaseApp ? getFirestore(firebaseApp) : null;
|
|
49
|
+
|
|
50
|
+
export function getFirebaseAuth(): Auth {
|
|
51
|
+
if (!isFirebaseConfigured || !firebaseAuth) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'Set EXPO_PUBLIC_FIREBASE_API_KEY, EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN, EXPO_PUBLIC_FIREBASE_PROJECT_ID, and EXPO_PUBLIC_FIREBASE_APP_ID before using Firebase.',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return firebaseAuth;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function assertFirebaseConfigured(): void {
|
|
60
|
+
void getFirebaseAuth();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getFirebaseDb(): Firestore {
|
|
64
|
+
if (!isFirebaseConfigured || !firebaseDb) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'Set EXPO_PUBLIC_FIREBASE_API_KEY, EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN, EXPO_PUBLIC_FIREBASE_PROJECT_ID, and EXPO_PUBLIC_FIREBASE_APP_ID before using Firebase.',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return firebaseDb;
|
|
70
|
+
}
|