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",