mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
Add guest auth and save-account flow
Introduce anonymous sign-in with in-app saving, guest recovery warnings, paywall prompts, and admin guest labeling.
This commit is contained in:
1 parent
ce68d57a49
commit
349f460e15
14 files changed
+1236
-176
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { View, ScrollView, StyleSheet, Platform } from 'react-native';
|
||||
import React from 'react';
|
||||
import { View, ScrollView, StyleSheet, Platform } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
import { GradientBackground } from '../../../components/ui';
|
||||
import { colors, spacing } from '../../../theme';
|
||||
@@ -10,9 +11,10 @@ import { ScreenHeader, SettingsSection, MenuItem, DangerZone } from '../componen
|
||||
/**
|
||||
* Account settings sub-screen (sign out, delete account).
|
||||
*/
|
||||
export function AccountScreen() {
|
||||
const { couple } = useAuthStore();
|
||||
const { handleSignOut, handleDeleteAccount } = useCoupleManagement();
|
||||
export function AccountScreen() {
|
||||
const router = useRouter();
|
||||
const { couple, isAnonymous } = useAuthStore();
|
||||
const { handleSignOut, handleDeleteAccount } = useCoupleManagement();
|
||||
|
||||
return (
|
||||
<GradientBackground>
|
||||
@@ -22,15 +24,25 @@ export function AccountScreen() {
|
||||
contentContainerStyle={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<SettingsSection title="Account" delay={0}>
|
||||
<MenuItem
|
||||
icon="log-out-outline"
|
||||
label="Sign Out"
|
||||
onPress={handleSignOut}
|
||||
variant="danger"
|
||||
showChevron={false}
|
||||
/>
|
||||
</SettingsSection>
|
||||
<SettingsSection title="Account" delay={0}>
|
||||
{isAnonymous ? (
|
||||
<MenuItem
|
||||
icon="shield-checkmark-outline"
|
||||
label="Save Account"
|
||||
description="Protect access across devices"
|
||||
onPress={() => router.push("/(app)/settings/save-account" as any)}
|
||||
showChevron={false}
|
||||
/>
|
||||
) : (
|
||||
<MenuItem
|
||||
icon="log-out-outline"
|
||||
label="Sign Out"
|
||||
onPress={handleSignOut}
|
||||
variant="danger"
|
||||
showChevron={false}
|
||||
/>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
<View style={styles.spacer} />
|
||||
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
Alert,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import * as Linking from "expo-linking";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
// Safely import platform auth modules
|
||||
let AppleAuthentication: typeof import("expo-apple-authentication") | null = null;
|
||||
let GoogleSignin: typeof import("@react-native-google-signin/google-signin").GoogleSignin | null = null;
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
try {
|
||||
AppleAuthentication = require("expo-apple-authentication");
|
||||
} catch {
|
||||
AppleAuthentication = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
try {
|
||||
const googleModule = require("@react-native-google-signin/google-signin");
|
||||
GoogleSignin = googleModule.GoogleSignin;
|
||||
if (GoogleSignin) {
|
||||
GoogleSignin.configure({
|
||||
webClientId: "764866133492-e78o2rh3rdjc4rfj2j6r2j77b065kggm.apps.googleusercontent.com",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
GoogleSignin = null;
|
||||
}
|
||||
}
|
||||
|
||||
import { GradientBackground, GlassButton, GlassCard, GlassInput } from "../../../components/ui";
|
||||
import { colors, spacing, radius, typography } from "../../../theme";
|
||||
import { supabase } from "../../../lib/supabase";
|
||||
import { getAuthError } from "../../../lib/errors";
|
||||
import { useAuthStore } from "../../../store";
|
||||
import { ScreenHeader } from "../components";
|
||||
|
||||
type SaveMode = "magic-link" | "password";
|
||||
|
||||
export function SaveAccountScreen() {
|
||||
const router = useRouter();
|
||||
const { isAnonymous, fetchUser } = useAuthStore();
|
||||
|
||||
const [mode, setMode] = useState<SaveMode>("magic-link");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [pendingEmail, setPendingEmail] = useState<string | null>(null);
|
||||
const [pendingPassword, setPendingPassword] = useState<string | null>(null);
|
||||
|
||||
const [isAppleAvailable, setIsAppleAvailable] = useState(false);
|
||||
const [isGoogleAvailable, setIsGoogleAvailable] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS === "ios" && AppleAuthentication) {
|
||||
AppleAuthentication.isAvailableAsync()
|
||||
.then(setIsAppleAvailable)
|
||||
.catch(() => setIsAppleAvailable(false));
|
||||
}
|
||||
if (Platform.OS === "android" && GoogleSignin) {
|
||||
setIsGoogleAvailable(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const emailRedirectTo = useMemo(() => Linking.createURL("/(auth)/login"), []);
|
||||
|
||||
const showError = useCallback((message: string) => {
|
||||
Alert.alert("Error", message);
|
||||
}, []);
|
||||
|
||||
const showSuccess = useCallback((message: string) => {
|
||||
Alert.alert("Success", message);
|
||||
}, []);
|
||||
|
||||
const syncProfileEmail = useCallback(async (userId: string, nextEmail: string | null | undefined) => {
|
||||
if (!nextEmail) return;
|
||||
try {
|
||||
await supabase
|
||||
.from("profiles")
|
||||
.update({ email: nextEmail })
|
||||
.eq("id", userId);
|
||||
} catch {
|
||||
// Non-blocking (some envs may restrict this by RLS)
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSaveWithEmail = useCallback(async () => {
|
||||
if (!email.trim()) {
|
||||
showError("Please enter your email");
|
||||
return;
|
||||
}
|
||||
if (mode === "password" && password.length < 8) {
|
||||
showError("Password must be at least 8 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
const { error } = await supabase.auth.updateUser(
|
||||
{ email: email.trim() },
|
||||
{ emailRedirectTo } as any
|
||||
);
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (error) {
|
||||
const friendly = getAuthError(error);
|
||||
if (__DEV__ && friendly === "Something went wrong. Please try again.") {
|
||||
showError(`${friendly}\n\n${error.message}`);
|
||||
} else {
|
||||
showError(friendly);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingEmail(email.trim());
|
||||
setPendingPassword(mode === "password" ? password : null);
|
||||
|
||||
Alert.alert(
|
||||
"Check your email",
|
||||
"We sent you a link to confirm your email. Once you confirm, come back and tap “I’ve verified my email”."
|
||||
);
|
||||
}, [email, password, mode, emailRedirectTo, showError]);
|
||||
|
||||
const handleCheckVerification = useCallback(async () => {
|
||||
setIsChecking(true);
|
||||
const { data: { user }, error } = await supabase.auth.getUser();
|
||||
setIsChecking(false);
|
||||
|
||||
if (error || !user) {
|
||||
showError(getAuthError(error) || "Unable to check account status");
|
||||
return;
|
||||
}
|
||||
|
||||
const isStillAnonymous = !!(user as any).is_anonymous;
|
||||
if (isStillAnonymous) {
|
||||
Alert.alert("Not yet verified", "Please click the link in your email to finish saving your account.");
|
||||
return;
|
||||
}
|
||||
|
||||
// If user chose password mode, set password after email is verified
|
||||
if (pendingPassword) {
|
||||
setIsSubmitting(true);
|
||||
const { error: passwordError } = await supabase.auth.updateUser({ password: pendingPassword });
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (passwordError) {
|
||||
showError(getAuthError(passwordError));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await syncProfileEmail(user.id, user.email);
|
||||
await fetchUser();
|
||||
|
||||
showSuccess("Account saved. You can now recover your account on any device.");
|
||||
router.navigate("/(app)/profile" as any);
|
||||
}, [pendingPassword, fetchUser, showError, showSuccess, router, syncProfileEmail]);
|
||||
|
||||
const handleLinkApple = useCallback(async () => {
|
||||
if (!AppleAuthentication) return;
|
||||
|
||||
try {
|
||||
const credential = await AppleAuthentication.signInAsync({
|
||||
requestedScopes: [
|
||||
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
|
||||
AppleAuthentication.AppleAuthenticationScope.EMAIL,
|
||||
],
|
||||
});
|
||||
|
||||
if (!credential.identityToken) {
|
||||
showError("No identity token received from Apple");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
const { error } = await (supabase.auth.linkIdentity as any)({
|
||||
provider: "apple",
|
||||
token: credential.identityToken,
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (error) {
|
||||
showError(getAuthError(error));
|
||||
return;
|
||||
}
|
||||
|
||||
// Apple only provides name/email on first auth grant
|
||||
if (credential.fullName) {
|
||||
const nameParts = [
|
||||
credential.fullName.givenName,
|
||||
credential.fullName.middleName,
|
||||
credential.fullName.familyName,
|
||||
].filter(Boolean);
|
||||
const fullName = nameParts.join(" ");
|
||||
|
||||
if (fullName) {
|
||||
await supabase.auth.updateUser({
|
||||
data: {
|
||||
full_name: fullName,
|
||||
given_name: credential.fullName.givenName,
|
||||
family_name: credential.fullName.familyName,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh user/profile
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
await syncProfileEmail(user.id, user.email);
|
||||
}
|
||||
await fetchUser();
|
||||
|
||||
showSuccess("Account saved with Apple.");
|
||||
router.navigate("/(app)/profile" as any);
|
||||
} catch (e: any) {
|
||||
if (e?.code === "ERR_REQUEST_CANCELED") return;
|
||||
showError("Apple sign in failed. Please try again.");
|
||||
}
|
||||
}, [fetchUser, router, showError, showSuccess, syncProfileEmail]);
|
||||
|
||||
const handleLinkGoogle = useCallback(async () => {
|
||||
if (!GoogleSignin) return;
|
||||
|
||||
try {
|
||||
await GoogleSignin.hasPlayServices();
|
||||
const response = await GoogleSignin.signIn();
|
||||
|
||||
if (response.type !== "success" || !response.data.idToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
const { error } = await (supabase.auth.linkIdentity as any)({
|
||||
provider: "google",
|
||||
token: response.data.idToken,
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (error) {
|
||||
showError(getAuthError(error));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
await syncProfileEmail(user.id, user.email);
|
||||
}
|
||||
await fetchUser();
|
||||
|
||||
showSuccess("Account saved with Google.");
|
||||
router.navigate("/(app)/profile" as any);
|
||||
} catch {
|
||||
showError("Google sign in failed. Please try again.");
|
||||
}
|
||||
}, [fetchUser, router, showError, showSuccess, syncProfileEmail]);
|
||||
|
||||
const showSocial = Platform.select({ ios: true, android: true, default: false });
|
||||
|
||||
if (!isAnonymous) {
|
||||
return (
|
||||
<GradientBackground>
|
||||
<ScreenHeader title="Save Account" />
|
||||
<View style={styles.centered}>
|
||||
<GlassCard variant="elevated" style={styles.readyCard}>
|
||||
<Ionicons name="checkmark-circle" size={36} color={colors.success} />
|
||||
<Text style={styles.readyTitle}>Your account is already saved</Text>
|
||||
<Text style={styles.readyBody}>
|
||||
You're signed in with a recoverable account.
|
||||
</Text>
|
||||
<GlassButton onPress={() => router.navigate("/(app)/profile" as any)}>
|
||||
Back to Settings
|
||||
</GlassButton>
|
||||
</GlassCard>
|
||||
</View>
|
||||
</GradientBackground>
|
||||
);
|
||||
}
|
||||
|
||||
if (pendingEmail) {
|
||||
return (
|
||||
<GradientBackground>
|
||||
<ScreenHeader title="Save Account" />
|
||||
<View style={styles.centered}>
|
||||
<GlassCard variant="elevated" style={styles.verifyCard}>
|
||||
<View style={styles.verifyIcon}>
|
||||
<Ionicons name="mail-unread" size={36} color={colors.primary} />
|
||||
</View>
|
||||
<Text style={styles.verifyTitle}>Confirm your email</Text>
|
||||
<Text style={styles.verifyBody}>
|
||||
We sent a confirmation link to {pendingEmail}. Tap it, then come back here.
|
||||
</Text>
|
||||
|
||||
<GlassButton
|
||||
onPress={handleCheckVerification}
|
||||
loading={isChecking || isSubmitting}
|
||||
disabled={isChecking || isSubmitting}
|
||||
fullWidth
|
||||
>
|
||||
I've verified my email
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
variant="secondary"
|
||||
onPress={handleSaveWithEmail}
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
style={{ marginTop: spacing.sm }}
|
||||
>
|
||||
Resend email
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setPendingEmail(null);
|
||||
setPendingPassword(null);
|
||||
}}
|
||||
fullWidth
|
||||
style={{ marginTop: spacing.sm }}
|
||||
>
|
||||
Use a different email
|
||||
</GlassButton>
|
||||
</GlassCard>
|
||||
</View>
|
||||
</GradientBackground>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GradientBackground>
|
||||
<ScreenHeader title="Save Account" />
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<GlassCard variant="elevated" style={styles.heroCard}>
|
||||
<Text style={styles.heroTitle}>Protect your account</Text>
|
||||
<Text style={styles.heroBody}>
|
||||
Unsaved accounts can't be recovered if you delete the app or switch devices.
|
||||
Save your account to keep your couple, matches, and purchases.
|
||||
</Text>
|
||||
</GlassCard>
|
||||
|
||||
{showSocial && (
|
||||
<GlassCard style={styles.sectionCard}>
|
||||
<Text style={styles.sectionTitle}>Quick save</Text>
|
||||
|
||||
{Platform.OS === "ios" && (
|
||||
<View style={styles.socialRow}>
|
||||
{isAppleAvailable && AppleAuthentication ? (
|
||||
<AppleAuthentication.AppleAuthenticationButton
|
||||
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
|
||||
buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.WHITE}
|
||||
cornerRadius={12}
|
||||
style={styles.appleButton}
|
||||
onPress={handleLinkApple}
|
||||
/>
|
||||
) : (
|
||||
<GlassButton
|
||||
variant="secondary"
|
||||
onPress={handleLinkApple}
|
||||
icon={<Ionicons name="logo-apple" size={20} color={colors.text} />}
|
||||
fullWidth
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Save with Apple
|
||||
</GlassButton>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{Platform.OS === "android" && isGoogleAvailable && (
|
||||
<GlassButton
|
||||
variant="secondary"
|
||||
onPress={handleLinkGoogle}
|
||||
icon={<Ionicons name="logo-google" size={20} color={colors.text} />}
|
||||
fullWidth
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Save with Google
|
||||
</GlassButton>
|
||||
)}
|
||||
|
||||
<Text style={styles.sectionHint}>
|
||||
Use an account that isn't already linked to Sauci.
|
||||
</Text>
|
||||
</GlassCard>
|
||||
)}
|
||||
|
||||
<GlassCard style={styles.sectionCard}>
|
||||
<Text style={styles.sectionTitle}>Save with email</Text>
|
||||
|
||||
<View style={styles.modeToggleContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modeToggle, mode === "magic-link" && styles.modeToggleActive]}
|
||||
onPress={() => setMode("magic-link")}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.modeToggleText, mode === "magic-link" && styles.modeToggleTextActive]}>
|
||||
Magic Link
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.modeToggle, mode === "password" && styles.modeToggleActive]}
|
||||
onPress={() => setMode("password")}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.modeToggleText, mode === "password" && styles.modeToggleTextActive]}>
|
||||
Password
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<GlassInput
|
||||
placeholder="Enter your email"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
icon={<Ionicons name="mail-outline" size={20} color={colors.textTertiary} />}
|
||||
/>
|
||||
|
||||
{mode === "password" && (
|
||||
<GlassInput
|
||||
placeholder="Create a password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
autoCapitalize="none"
|
||||
icon={<Ionicons name="lock-closed-outline" size={20} color={colors.textTertiary} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<GlassButton
|
||||
onPress={handleSaveWithEmail}
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
>
|
||||
Save with Email
|
||||
</GlassButton>
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionHint}>
|
||||
We'll send a confirmation link to your email.
|
||||
</Text>
|
||||
</GlassCard>
|
||||
|
||||
<View style={{ height: spacing.xl }} />
|
||||
</ScrollView>
|
||||
</GradientBackground>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
paddingTop: spacing.lg,
|
||||
paddingBottom: Platform.OS === "ios" ? 40 : 20,
|
||||
gap: spacing.lg,
|
||||
},
|
||||
heroCard: {
|
||||
marginHorizontal: spacing.lg,
|
||||
},
|
||||
heroTitle: {
|
||||
...typography.title3,
|
||||
color: colors.text,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
heroBody: {
|
||||
...typography.body,
|
||||
color: colors.textSecondary,
|
||||
lineHeight: 22,
|
||||
},
|
||||
sectionCard: {
|
||||
marginHorizontal: spacing.lg,
|
||||
},
|
||||
sectionTitle: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
sectionHint: {
|
||||
...typography.caption1,
|
||||
color: colors.textTertiary,
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
socialRow: {
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
appleButton: {
|
||||
width: "100%",
|
||||
height: 48,
|
||||
},
|
||||
modeToggleContainer: {
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.glass.background,
|
||||
borderRadius: radius.md,
|
||||
padding: 4,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
modeToggle: {
|
||||
flex: 1,
|
||||
paddingVertical: 12,
|
||||
borderRadius: radius.sm,
|
||||
alignItems: "center",
|
||||
},
|
||||
modeToggleActive: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
modeToggleText: {
|
||||
...typography.subhead,
|
||||
color: colors.textTertiary,
|
||||
fontWeight: "600",
|
||||
},
|
||||
modeToggleTextActive: {
|
||||
color: colors.text,
|
||||
},
|
||||
form: {
|
||||
gap: spacing.sm,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: spacing.lg,
|
||||
},
|
||||
readyCard: {
|
||||
width: "100%",
|
||||
maxWidth: 420,
|
||||
alignItems: "center",
|
||||
gap: spacing.sm,
|
||||
},
|
||||
readyTitle: {
|
||||
...typography.title3,
|
||||
color: colors.text,
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
readyBody: {
|
||||
...typography.body,
|
||||
color: colors.textSecondary,
|
||||
textAlign: "center",
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
verifyCard: {
|
||||
width: "100%",
|
||||
maxWidth: 420,
|
||||
alignItems: "center",
|
||||
padding: spacing.lg,
|
||||
},
|
||||
verifyIcon: {
|
||||
width: 90,
|
||||
height: 90,
|
||||
borderRadius: 45,
|
||||
backgroundColor: colors.primaryLight,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
verifyTitle: {
|
||||
...typography.title3,
|
||||
color: colors.text,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
verifyBody: {
|
||||
...typography.body,
|
||||
color: colors.textSecondary,
|
||||
textAlign: "center",
|
||||
lineHeight: 22,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
});
|
||||
|
||||
export default SaveAccountScreen;
|
||||
@@ -5,4 +5,5 @@ export { PreferencesScreen } from './PreferencesScreen';
|
||||
export { SubscriptionScreen } from './SubscriptionScreen';
|
||||
export { PartnerScreen } from './PartnerScreen';
|
||||
export { SupportScreen } from './SupportScreen';
|
||||
export { AccountScreen } from './AccountScreen';
|
||||
export { AccountScreen } from './AccountScreen';
|
||||
export { SaveAccountScreen } from './SaveAccountScreen';
|
||||
Reference in new issue
Block a user