diff --git a/app.json b/app.json new file mode 100644 index 0000000..146b016 --- /dev/null +++ b/app.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "expo-asset" + ] +} diff --git a/apps/mobile/app.json b/apps/mobile/app.json index ada0d16..aa7e8fc 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -65,7 +65,8 @@ } ], "@sentry/react-native", - "expo-font" + "expo-font", + "expo-asset" ], "experiments": { "typedRoutes": true diff --git a/apps/mobile/app/(app)/profile.tsx b/apps/mobile/app/(app)/profile.tsx index 82e53ed..d8d780a 100755 --- a/apps/mobile/app/(app)/profile.tsx +++ b/apps/mobile/app/(app)/profile.tsx @@ -392,17 +392,26 @@ export default function ProfileScreen() { )} - {/* Restore Purchases Link */} + {/* Restore Purchases & Redeem Links */} {!hasPremiumAccess && ( - - - {isPurchasing ? "Restoring..." : "Restore Purchases"} - - + + + + {isPurchasing ? "Restoring..." : "Restore Purchases"} + + + | + router.push("/(app)/redeem")} + > + Redeem Code + + )} @@ -896,15 +905,23 @@ const styles = StyleSheet.create({ color: colors.text, fontWeight: "600", }, - restoreLink: { + subscriptionLinks: { + flexDirection: "row", + justifyContent: "center", alignItems: "center", marginTop: spacing.sm, + }, + restoreLink: { padding: spacing.sm, }, restoreLinkText: { ...typography.caption1, color: colors.textTertiary, }, + linkDivider: { + ...typography.caption1, + color: colors.textTertiary, + }, versionContainer: { alignItems: "center", marginTop: spacing.lg, diff --git a/apps/mobile/app/(app)/redeem.tsx b/apps/mobile/app/(app)/redeem.tsx new file mode 100644 index 0000000..1e591ad --- /dev/null +++ b/apps/mobile/app/(app)/redeem.tsx @@ -0,0 +1,312 @@ +import { useState } from "react"; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + Alert, + KeyboardAvoidingView, + Platform, + ScrollView, +} from "react-native"; +import { router } from "expo-router"; +import Animated, { + FadeInDown, + FadeInUp, +} from "react-native-reanimated"; +import { Ionicons } from "@expo/vector-icons"; +import { GradientBackground, GlassCard, GlassButton, GlassInput } from "../../src/components/ui"; +import { colors, spacing, radius, typography } from "../../src/theme"; + +export default function RedeemScreen() { + const [email, setEmail] = useState(""); + const [code, setCode] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isRedeemed, setIsRedeemed] = useState(false); + const [error, setError] = useState(null); + + const showError = (message: string) => { + setError(message); + if (Platform.OS !== 'web') { + Alert.alert("Error", message); + } + }; + + const clearError = () => setError(null); + + const handleRedeem = async () => { + clearError(); + + if (!email.trim()) { + showError("Please enter your email address"); + return; + } + + if (!code.trim()) { + showError("Please enter your redemption code"); + return; + } + + // Basic email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email.trim())) { + showError("Please enter a valid email address"); + return; + } + + setIsLoading(true); + + // TODO: Wire up to backend + // For now, just simulate a delay and show success + await new Promise(resolve => setTimeout(resolve, 1500)); + + setIsLoading(false); + setIsRedeemed(true); + }; + + // Success screen + if (isRedeemed) { + return ( + + + + + + + Code Redeemed! + + Your premium access has been activated for{'\n'} + {email} + + + Enjoy all premium question packs and features. + + router.back()} + style={{ marginTop: spacing.lg }} + > + Back to Profile + + + + + ); + } + + return ( + + + + + {/* Back Button */} + + router.back()} + activeOpacity={0.7} + > + + + + + {/* Header */} + + + + + Redeem Premium + + Enter your email and redemption code to unlock premium features + + + + {/* Form Card */} + + + {/* Error Message */} + {error && ( + + + {error} + + )} + + {/* Form Fields */} + + { clearError(); setEmail(text); }} + keyboardType="email-address" + autoCapitalize="none" + autoCorrect={false} + icon={} + /> + + { clearError(); setCode(text.toUpperCase()); }} + autoCapitalize="characters" + autoCorrect={false} + icon={} + /> + + + Redeem Code + + + + + + {/* Info Text */} + + + + Redemption codes are provided through promotions and giveaways. Each code can only be used once. + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + centeredContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + centeredContent: { + alignItems: "center", + paddingHorizontal: spacing.lg, + }, + scrollContent: { + flexGrow: 1, + }, + content: { + flex: 1, + paddingHorizontal: spacing.lg, + paddingTop: 60, + paddingBottom: spacing.xl, + }, + backButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: colors.glass.background, + justifyContent: "center", + alignItems: "center", + marginBottom: spacing.lg, + }, + headerContainer: { + alignItems: "center", + marginBottom: spacing.xl, + }, + logoContainer: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: colors.primaryLight, + justifyContent: "center", + alignItems: "center", + marginBottom: spacing.md, + }, + iconContainer: { + width: 100, + height: 100, + borderRadius: 50, + backgroundColor: colors.successLight, + justifyContent: "center", + alignItems: "center", + marginBottom: spacing.lg, + }, + title: { + ...typography.largeTitle, + color: colors.text, + marginBottom: spacing.xs, + textAlign: "center", + }, + subtitle: { + ...typography.body, + color: colors.textSecondary, + textAlign: "center", + }, + emailHighlight: { + color: colors.primary, + fontWeight: "600", + }, + hint: { + ...typography.subhead, + color: colors.textSecondary, + textAlign: "center", + marginTop: spacing.md, + paddingHorizontal: spacing.md, + }, + formContainer: { + marginBottom: spacing.lg, + }, + errorContainer: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.errorLight, + padding: spacing.md, + borderRadius: radius.md, + marginBottom: spacing.md, + gap: spacing.sm, + }, + errorText: { + ...typography.subhead, + color: colors.error, + flex: 1, + }, + form: { + gap: spacing.sm, + }, + infoContainer: { + flexDirection: "row", + alignItems: "flex-start", + paddingHorizontal: spacing.sm, + gap: spacing.sm, + }, + infoText: { + ...typography.caption1, + color: colors.textTertiary, + flex: 1, + lineHeight: 18, + }, +}); diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js new file mode 100644 index 0000000..471d972 --- /dev/null +++ b/apps/mobile/babel.config.js @@ -0,0 +1,6 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + }; +}; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index a0c6f46..13800da 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -21,9 +21,11 @@ "@sentry/react-native": "~6.10.0", "@supabase/supabase-js": "^2.39.0", "@tanstack/react-query": "^5.17.0", + "babel-preset-expo": "~12.0.0", "base64-arraybuffer": "^1.0.2", "expo": "~52.0.0", "expo-apple-authentication": "~7.1.3", + "expo-asset": "~11.0.5", "expo-blur": "~14.0.3", "expo-build-properties": "^0.13.3", "expo-clipboard": "~7.0.1", diff --git a/package-lock.json b/package-lock.json index fdce77f..3f31c6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -133,9 +133,11 @@ "@sentry/react-native": "~6.10.0", "@supabase/supabase-js": "^2.39.0", "@tanstack/react-query": "^5.17.0", + "babel-preset-expo": "~12.0.0", "base64-arraybuffer": "^1.0.2", "expo": "~52.0.0", "expo-apple-authentication": "~7.1.3", + "expo-asset": "~11.0.5", "expo-blur": "~14.0.3", "expo-build-properties": "^0.13.3", "expo-clipboard": "~7.0.1", @@ -324,20 +326,6 @@ "react-native": "*" } }, - "apps/mobile/node_modules/expo-constants": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.8.tgz", - "integrity": "sha512-XfWRyQAf1yUNgWZ1TnE8pFBMqGmFP5Gb+SFSgszxDdOoheB/NI5D4p7q86kI2fvGyfTrxAe+D+74nZkfsGvUlg==", - "license": "MIT", - "dependencies": { - "@expo/config": "~10.0.11", - "@expo/env": "~0.4.2" - }, - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, "apps/mobile/node_modules/expo-file-system": { "version": "18.0.12", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.12.tgz", @@ -7821,6 +7809,17 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-react-compiler": { + "version": "19.0.0-beta-ebf51a3-20250411", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.0.0-beta-ebf51a3-20250411.tgz", + "integrity": "sha512-q84bNR9JG1crykAlJUt5Ud0/5BUyMFuQww/mrwIQDFBaxsikqBDj3f/FNDsVd2iR26A1HvXKWPEIfgJDv8/V2g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.26.0" + } + }, "node_modules/babel-plugin-react-native-web": { "version": "0.19.13", "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.19.13.tgz", @@ -9924,6 +9923,23 @@ "react-native": "*" } }, + "node_modules/expo-asset": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.0.5.tgz", + "integrity": "sha512-TL60LmMBGVzs3NQcO8ylWqBumMh4sx0lmeJsn7+9C88fylGDhyyVnKZ1PyTXo9CVDBkndutZx2JUEQWM9BaiXw==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.6.5", + "expo-constants": "~17.0.8", + "invariant": "^2.2.4", + "md5-file": "^3.2.3" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-build-properties": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-0.13.3.tgz", @@ -9937,6 +9953,20 @@ "expo": "*" } }, + "node_modules/expo-constants": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.8.tgz", + "integrity": "sha512-XfWRyQAf1yUNgWZ1TnE8pFBMqGmFP5Gb+SFSgszxDdOoheB/NI5D4p7q86kI2fvGyfTrxAe+D+74nZkfsGvUlg==", + "license": "MIT", + "dependencies": { + "@expo/config": "~10.0.11", + "@expo/env": "~0.4.2" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-dev-client": { "version": "5.0.20", "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-5.0.20.tgz", @@ -10131,37 +10161,6 @@ "expo": "*" } }, - "node_modules/expo/node_modules/expo-asset": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.0.5.tgz", - "integrity": "sha512-TL60LmMBGVzs3NQcO8ylWqBumMh4sx0lmeJsn7+9C88fylGDhyyVnKZ1PyTXo9CVDBkndutZx2JUEQWM9BaiXw==", - "license": "MIT", - "dependencies": { - "@expo/image-utils": "^0.6.5", - "expo-constants": "~17.0.8", - "invariant": "^2.2.4", - "md5-file": "^3.2.3" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/expo-constants": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.8.tgz", - "integrity": "sha512-XfWRyQAf1yUNgWZ1TnE8pFBMqGmFP5Gb+SFSgszxDdOoheB/NI5D4p7q86kI2fvGyfTrxAe+D+74nZkfsGvUlg==", - "license": "MIT", - "dependencies": { - "@expo/config": "~10.0.11", - "@expo/env": "~0.4.2" - }, - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, "node_modules/expo/node_modules/expo-file-system": { "version": "18.0.12", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.12.tgz",