create-stackr 0.2.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.
Files changed (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +642 -0
  3. package/bin/cli.js +12 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +113 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config/dependencies.d.ts +82 -0
  9. package/dist/config/dependencies.d.ts.map +1 -0
  10. package/dist/config/dependencies.js +82 -0
  11. package/dist/config/dependencies.js.map +1 -0
  12. package/dist/config/presets.d.ts +3 -0
  13. package/dist/config/presets.d.ts.map +1 -0
  14. package/dist/config/presets.js +174 -0
  15. package/dist/config/presets.js.map +1 -0
  16. package/dist/generators/index.d.ts +40 -0
  17. package/dist/generators/index.d.ts.map +1 -0
  18. package/dist/generators/index.js +130 -0
  19. package/dist/generators/index.js.map +1 -0
  20. package/dist/generators/onboarding.d.ts +8 -0
  21. package/dist/generators/onboarding.d.ts.map +1 -0
  22. package/dist/generators/onboarding.js +141 -0
  23. package/dist/generators/onboarding.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +65 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/prompts/features.d.ts +14 -0
  29. package/dist/prompts/features.d.ts.map +1 -0
  30. package/dist/prompts/features.js +96 -0
  31. package/dist/prompts/features.js.map +1 -0
  32. package/dist/prompts/index.d.ts +3 -0
  33. package/dist/prompts/index.d.ts.map +1 -0
  34. package/dist/prompts/index.js +93 -0
  35. package/dist/prompts/index.js.map +1 -0
  36. package/dist/prompts/onboarding.d.ts +6 -0
  37. package/dist/prompts/onboarding.d.ts.map +1 -0
  38. package/dist/prompts/onboarding.js +37 -0
  39. package/dist/prompts/onboarding.js.map +1 -0
  40. package/dist/prompts/orm.d.ts +3 -0
  41. package/dist/prompts/orm.d.ts.map +1 -0
  42. package/dist/prompts/orm.js +23 -0
  43. package/dist/prompts/orm.js.map +1 -0
  44. package/dist/prompts/packageManager.d.ts +2 -0
  45. package/dist/prompts/packageManager.d.ts.map +1 -0
  46. package/dist/prompts/packageManager.js +18 -0
  47. package/dist/prompts/packageManager.js.map +1 -0
  48. package/dist/prompts/platform.d.ts +3 -0
  49. package/dist/prompts/platform.d.ts.map +1 -0
  50. package/dist/prompts/platform.js +21 -0
  51. package/dist/prompts/platform.js.map +1 -0
  52. package/dist/prompts/preset.d.ts +4 -0
  53. package/dist/prompts/preset.d.ts.map +1 -0
  54. package/dist/prompts/preset.js +165 -0
  55. package/dist/prompts/preset.js.map +1 -0
  56. package/dist/prompts/project.d.ts +2 -0
  57. package/dist/prompts/project.d.ts.map +1 -0
  58. package/dist/prompts/project.js +27 -0
  59. package/dist/prompts/project.js.map +1 -0
  60. package/dist/prompts/sdks.d.ts +2 -0
  61. package/dist/prompts/sdks.d.ts.map +1 -0
  62. package/dist/prompts/sdks.js +46 -0
  63. package/dist/prompts/sdks.js.map +1 -0
  64. package/dist/types/index.d.ts +77 -0
  65. package/dist/types/index.d.ts.map +1 -0
  66. package/dist/types/index.js +25 -0
  67. package/dist/types/index.js.map +1 -0
  68. package/dist/utils/cleanup.d.ts +5 -0
  69. package/dist/utils/cleanup.d.ts.map +1 -0
  70. package/dist/utils/cleanup.js +38 -0
  71. package/dist/utils/cleanup.js.map +1 -0
  72. package/dist/utils/copy.d.ts +10 -0
  73. package/dist/utils/copy.d.ts.map +1 -0
  74. package/dist/utils/copy.js +53 -0
  75. package/dist/utils/copy.js.map +1 -0
  76. package/dist/utils/errors.d.ts +33 -0
  77. package/dist/utils/errors.d.ts.map +1 -0
  78. package/dist/utils/errors.js +136 -0
  79. package/dist/utils/errors.js.map +1 -0
  80. package/dist/utils/git.d.ts +5 -0
  81. package/dist/utils/git.d.ts.map +1 -0
  82. package/dist/utils/git.js +33 -0
  83. package/dist/utils/git.js.map +1 -0
  84. package/dist/utils/logger.d.ts +9 -0
  85. package/dist/utils/logger.d.ts.map +1 -0
  86. package/dist/utils/logger.js +22 -0
  87. package/dist/utils/logger.js.map +1 -0
  88. package/dist/utils/package.d.ts +16 -0
  89. package/dist/utils/package.d.ts.map +1 -0
  90. package/dist/utils/package.js +86 -0
  91. package/dist/utils/package.js.map +1 -0
  92. package/dist/utils/system-validation.d.ts +9 -0
  93. package/dist/utils/system-validation.d.ts.map +1 -0
  94. package/dist/utils/system-validation.js +31 -0
  95. package/dist/utils/system-validation.js.map +1 -0
  96. package/dist/utils/template.d.ts +20 -0
  97. package/dist/utils/template.d.ts.map +1 -0
  98. package/dist/utils/template.js +234 -0
  99. package/dist/utils/template.js.map +1 -0
  100. package/dist/utils/validation.d.ts +8 -0
  101. package/dist/utils/validation.d.ts.map +1 -0
  102. package/dist/utils/validation.js +94 -0
  103. package/dist/utils/validation.js.map +1 -0
  104. package/package.json +96 -0
  105. package/templates/base/backend/.dockerignore.ejs +62 -0
  106. package/templates/base/backend/.env.example.ejs +116 -0
  107. package/templates/base/backend/Dockerfile.ejs +142 -0
  108. package/templates/base/backend/controllers/event-queue/index.ts +20 -0
  109. package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
  110. package/templates/base/backend/controllers/rest-api/index.ts +48 -0
  111. package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
  112. package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
  113. package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
  114. package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
  115. package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
  116. package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
  117. package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
  118. package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
  119. package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
  120. package/templates/base/backend/domain/device-session/schema.ts +72 -0
  121. package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
  122. package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
  123. package/templates/base/backend/domain/session/schema.ts +29 -0
  124. package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
  125. package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
  126. package/templates/base/backend/domain/user/schema.ts +14 -0
  127. package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
  128. package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
  129. package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
  130. package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
  131. package/templates/base/backend/lib/constants.ts.ejs +29 -0
  132. package/templates/base/backend/package.json.ejs +50 -0
  133. package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
  134. package/templates/base/backend/prisma.config.prisma.ts +12 -0
  135. package/templates/base/backend/tsconfig.json +39 -0
  136. package/templates/base/backend/utils/db.drizzle.ts +41 -0
  137. package/templates/base/backend/utils/db.prisma.ts +51 -0
  138. package/templates/base/backend/utils/email.ts.ejs +35 -0
  139. package/templates/base/backend/utils/errors.ts +348 -0
  140. package/templates/base/backend/utils/redis.ts.ejs +279 -0
  141. package/templates/base/mobile/.env.example.ejs +35 -0
  142. package/templates/base/mobile/.gitignore.ejs +167 -0
  143. package/templates/base/mobile/app/+not-found.tsx +85 -0
  144. package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
  145. package/templates/base/mobile/app.json.ejs +88 -0
  146. package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
  147. package/templates/base/mobile/assets/images/favicon.png +0 -0
  148. package/templates/base/mobile/assets/images/icon.png +0 -0
  149. package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
  150. package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
  151. package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
  152. package/templates/base/mobile/assets/images/paywall_image.png +0 -0
  153. package/templates/base/mobile/assets/images/splash.png +0 -0
  154. package/templates/base/mobile/eas.json.ejs +49 -0
  155. package/templates/base/mobile/metro.config.js +9 -0
  156. package/templates/base/mobile/package.json.ejs +53 -0
  157. package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
  158. package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
  159. package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
  160. package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
  161. package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
  162. package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
  163. package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
  164. package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
  165. package/templates/base/mobile/src/components/ui/index.ts +6 -0
  166. package/templates/base/mobile/src/constants/Theme.ts +163 -0
  167. package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
  168. package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
  169. package/templates/base/mobile/src/services/api.ts.ejs +71 -0
  170. package/templates/base/mobile/src/services/errorService.ts +179 -0
  171. package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
  172. package/templates/base/mobile/src/store/index.ts.ejs +18 -0
  173. package/templates/base/mobile/src/store/ui.store.ts +100 -0
  174. package/templates/base/mobile/src/utils/formatters.ts +105 -0
  175. package/templates/base/mobile/src/utils/logger.ts +73 -0
  176. package/templates/base/mobile/src/utils/responsive.ts +234 -0
  177. package/templates/base/mobile/tsconfig.json +32 -0
  178. package/templates/base/web/.env.example.ejs +26 -0
  179. package/templates/base/web/components.json +22 -0
  180. package/templates/base/web/eslint.config.mjs +18 -0
  181. package/templates/base/web/next.config.ts +7 -0
  182. package/templates/base/web/package.json.ejs +35 -0
  183. package/templates/base/web/postcss.config.mjs +7 -0
  184. package/templates/base/web/public/.gitkeep +0 -0
  185. package/templates/base/web/public/file.svg +1 -0
  186. package/templates/base/web/public/globe.svg +1 -0
  187. package/templates/base/web/public/next.svg +1 -0
  188. package/templates/base/web/public/vercel.svg +1 -0
  189. package/templates/base/web/public/window.svg +1 -0
  190. package/templates/base/web/src/app/favicon.ico +0 -0
  191. package/templates/base/web/src/app/globals.css +152 -0
  192. package/templates/base/web/src/app/layout.tsx.ejs +54 -0
  193. package/templates/base/web/src/app/page.tsx.ejs +92 -0
  194. package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
  195. package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
  196. package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
  197. package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
  198. package/templates/base/web/src/components/theme-toggle.tsx +34 -0
  199. package/templates/base/web/src/components/ui/button.tsx +62 -0
  200. package/templates/base/web/src/components/ui/card.tsx +92 -0
  201. package/templates/base/web/src/components/ui/input.tsx +21 -0
  202. package/templates/base/web/src/components/ui/label.tsx +24 -0
  203. package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
  204. package/templates/base/web/src/components/ui/spinner.tsx +20 -0
  205. package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
  206. package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
  207. package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
  208. package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
  209. package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
  210. package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
  211. package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
  212. package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
  213. package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
  214. package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
  215. package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
  216. package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
  217. package/templates/base/web/src/lib/utils.ts +6 -0
  218. package/templates/base/web/src/proxy.ts.ejs +66 -0
  219. package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
  220. package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
  221. package/templates/base/web/tsconfig.json +34 -0
  222. package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
  223. package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
  224. package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
  225. package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
  226. package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
  227. package/templates/features/mobile/auth/components/auth/index.ts +2 -0
  228. package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
  229. package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
  230. package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
  231. package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
  232. package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
  233. package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
  234. package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
  235. package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
  236. package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
  237. package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
  238. package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
  239. package/templates/features/web/.gitkeep +0 -0
  240. package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
  241. package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
  242. package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
  243. package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
  244. package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
  245. package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
  246. package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
  247. package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
  248. package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
  249. package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
  250. package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
  251. package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
  252. package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
  253. package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
  254. package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
  255. package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
  256. package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
  257. package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
  258. package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
  259. package/templates/integrations/mobile/att/services/attService.ts +84 -0
  260. package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
  261. package/templates/integrations/mobile/att/store/att.store.ts +162 -0
  262. package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
  263. package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
  264. package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
  265. package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
  266. package/templates/integrations/web/.gitkeep +0 -0
  267. package/templates/shared/.env.example.ejs +21 -0
  268. package/templates/shared/.gitignore.ejs +145 -0
  269. package/templates/shared/README.md.ejs +134 -0
  270. package/templates/shared/docker-compose.prod.yml.ejs +120 -0
  271. package/templates/shared/docker-compose.yml.ejs +129 -0
  272. package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
  273. package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
  274. package/templates/shared/scripts/setup.sh.ejs +979 -0
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { View, Image, StyleSheet } from 'react-native';
3
+ import { router } from 'expo-router';
4
+ import OnboardingLayout from '@/components/ui/OnboardingLayout';
5
+ import { responsive } from '@/utils/responsive';
6
+
7
+ export default function OnboardingPage1() {
8
+ const handleContinue = () => {
9
+ <% if (features.onboarding.pages === 1) { %>
10
+ <% if (features.paywall) { %>
11
+ router.replace('/paywall');
12
+ <% } else { %>
13
+ router.replace('/(tabs)');
14
+ <% } %>
15
+ <% } else { %>
16
+ router.replace('/(onboarding)/page-2');
17
+ <% } %>
18
+ };
19
+
20
+ return (
21
+ <OnboardingLayout
22
+ title="Welcome to Your App"
23
+ subtitle="Get started with powerful features designed to make your life easier."
24
+ pageIndicators={1}
25
+ totalPages={<%= features.onboarding.pages %>}
26
+ onContinue={handleContinue}
27
+ >
28
+ <View style={styles.imageContainer}>
29
+ <Image
30
+ source={require('@/assets/images/onboarding_page_1.png')}
31
+ style={styles.onboardingImage}
32
+ resizeMode="contain"
33
+ />
34
+ </View>
35
+ </OnboardingLayout>
36
+ );
37
+ }
38
+
39
+ const imageSize = responsive.getImageContainerSize();
40
+
41
+ const styles = StyleSheet.create({
42
+ imageContainer: {
43
+ width: imageSize.width,
44
+ height: imageSize.height,
45
+ justifyContent: 'center',
46
+ alignItems: 'center',
47
+ },
48
+ onboardingImage: {
49
+ width: '100%',
50
+ height: '100%',
51
+ },
52
+ });
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { View, Image, StyleSheet } from 'react-native';
3
+ import { router } from 'expo-router';
4
+ import OnboardingLayout from '@/components/ui/OnboardingLayout';
5
+ import { responsive } from '@/utils/responsive';
6
+
7
+ export default function OnboardingPage2() {
8
+ const handleContinue = () => {
9
+ <% if (features.onboarding.pages === 2) { %>
10
+ <% if (features.paywall) { %>
11
+ router.replace('/paywall');
12
+ <% } else { %>
13
+ router.replace('/(tabs)');
14
+ <% } %>
15
+ <% } else { %>
16
+ router.replace('/(onboarding)/page-3');
17
+ <% } %>
18
+ };
19
+
20
+ return (
21
+ <OnboardingLayout
22
+ title="Powerful Features"
23
+ subtitle="Unlock advanced capabilities and streamlined workflows to boost your productivity."
24
+ pageIndicators={2}
25
+ totalPages={<%= features.onboarding.pages %>}
26
+ onContinue={handleContinue}
27
+ >
28
+ <View style={styles.imageContainer}>
29
+ <Image
30
+ source={require('@/assets/images/onboarding_page_2.png')}
31
+ style={styles.onboardingImage}
32
+ resizeMode="contain"
33
+ />
34
+ </View>
35
+ </OnboardingLayout>
36
+ );
37
+ }
38
+
39
+ const imageSize = responsive.getImageContainerSize();
40
+
41
+ const styles = StyleSheet.create({
42
+ imageContainer: {
43
+ width: imageSize.width,
44
+ height: imageSize.height,
45
+ justifyContent: 'center',
46
+ alignItems: 'center',
47
+ },
48
+ onboardingImage: {
49
+ width: '100%',
50
+ height: '100%',
51
+ },
52
+ });
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { View, Image, StyleSheet } from 'react-native';
3
+ import { router } from 'expo-router';
4
+ import AsyncStorage from '@react-native-async-storage/async-storage';
5
+ import OnboardingLayout from '@/components/ui/OnboardingLayout';
6
+ import { responsive } from '@/utils/responsive';
7
+
8
+ export default function OnboardingPage3() {
9
+ const handleGetStarted = async () => {
10
+ try {
11
+ await AsyncStorage.setItem('onboarding_completed', 'true');
12
+ <% if (features.paywall) { %>
13
+ router.replace('/paywall');
14
+ <% } else { %>
15
+ router.replace('/(tabs)');
16
+ <% } %>
17
+ } catch (error) {
18
+ console.error('Failed to save onboarding completion status:', error);
19
+ <% if (features.paywall) { %>
20
+ router.replace('/paywall');
21
+ <% } else { %>
22
+ router.replace('/(tabs)');
23
+ <% } %>
24
+ }
25
+ };
26
+
27
+ return (
28
+ <OnboardingLayout
29
+ title="Ready to Get Started"
30
+ subtitle="You're all set! Let's unlock the full potential of your new experience."
31
+ pageIndicators={3}
32
+ totalPages={<%= features.onboarding.pages %>}
33
+ onContinue={handleGetStarted}
34
+ continueText="Get Started"
35
+ >
36
+ <View style={styles.imageContainer}>
37
+ <Image
38
+ source={require('@/assets/images/onboarding_page_3.png')}
39
+ style={styles.onboardingImage}
40
+ resizeMode="contain"
41
+ />
42
+ </View>
43
+ </OnboardingLayout>
44
+ );
45
+ }
46
+
47
+ const imageSize = responsive.getImageContainerSize();
48
+
49
+ const styles = StyleSheet.create({
50
+ imageContainer: {
51
+ width: imageSize.width,
52
+ height: imageSize.height,
53
+ justifyContent: 'center',
54
+ alignItems: 'center',
55
+ },
56
+ onboardingImage: {
57
+ width: '100%',
58
+ height: '100%',
59
+ },
60
+ });
@@ -0,0 +1,550 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ Alert,
8
+ ActivityIndicator,
9
+ } from 'react-native';
10
+ import { IconSymbol } from '../src/components/ui/IconSymbol';
11
+ import { Theme } from '@/constants/Theme';
12
+ import { router, Stack } from 'expo-router';
13
+ import { useRevenueCat, useRevenueCatActions } from '../src/store/revenuecat.store';
14
+ import AsyncStorage from '@react-native-async-storage/async-storage';
15
+ import { logger } from '../src/utils/logger';
16
+ import OnboardingLayout from '../src/components/ui/OnboardingLayout';
17
+ import { responsive, fontSize, getSpacing } from '@/utils/responsive';
18
+
19
+ interface PlanOption {
20
+ id: string;
21
+ title: string;
22
+ price: string;
23
+ period: string;
24
+ badge?: string;
25
+ revenueCatPackage: any;
26
+ pricePerWeek?: string;
27
+ }
28
+
29
+ export default function PaywallScreen() {
30
+ const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
31
+ const [isLoading, setIsLoading] = useState(false);
32
+
33
+ const {
34
+ customerInfo,
35
+ isLoading: subscriptionLoading,
36
+ offerings,
37
+ } = useRevenueCat();
38
+
39
+ const { getOfferings, purchasePackage, restorePurchases } = useRevenueCatActions();
40
+
41
+ const hasActiveSubscription = customerInfo?.entitlements.active ?
42
+ Object.keys(customerInfo.entitlements.active).length > 0 : false;
43
+
44
+ // Fetch offerings when component mounts
45
+ useEffect(() => {
46
+ getOfferings();
47
+ }, [getOfferings]);
48
+
49
+ const hasOfferings = useMemo(() => {
50
+ return offerings && Object.keys(offerings.all).length > 0;
51
+ }, [offerings]);
52
+
53
+ const plans: PlanOption[] = useMemo(() => {
54
+ const mappedPlans: PlanOption[] = [];
55
+
56
+ if (hasOfferings && offerings) {
57
+ // Use real offerings from RevenueCat
58
+ for (const key in offerings.all) {
59
+ if (offerings.all.hasOwnProperty(key)) {
60
+ const offering = offerings.all[key];
61
+ if (offering && offering.availablePackages?.length > 0) {
62
+ const pkg = offering.availablePackages[0];
63
+ let badge = '';
64
+ let pricePerWeek = '';
65
+
66
+ // Map plans according to receipt-scanner pattern
67
+ if (offering.serverDescription === 'Weekly') {
68
+ badge = 'Popular';
69
+ } else if (offering.serverDescription === 'Annual') {
70
+ badge = '80% OFF!';
71
+ // Get the weekly price from RevenueCat for annual plans
72
+ pricePerWeek = pkg.product.pricePerWeekString || '';
73
+ }
74
+
75
+ mappedPlans.push({
76
+ id: pkg.identifier,
77
+ title: offering.serverDescription === 'Weekly' ? '1 Week' :
78
+ offering.serverDescription === 'Annual' ? '1 Year' :
79
+ offering.serverDescription === 'Monthly' ? '1 Month' : offering.serverDescription,
80
+ price: pkg.product.priceString,
81
+ period: `per ${pkg.packageType.toLowerCase()}`,
82
+ badge,
83
+ pricePerWeek,
84
+ revenueCatPackage: pkg,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ } else {
90
+ // Use mock data for development
91
+ mappedPlans.push(
92
+ {
93
+ id: 'mock_weekly',
94
+ title: '1 Week',
95
+ price: '$4.99',
96
+ period: 'per week',
97
+ badge: 'Popular',
98
+ revenueCatPackage: null,
99
+ },
100
+ {
101
+ id: 'mock_annual',
102
+ title: '1 Year',
103
+ price: '$49.99',
104
+ period: 'per year',
105
+ badge: '80% OFF!',
106
+ pricePerWeek: '$0.96',
107
+ revenueCatPackage: null,
108
+ },
109
+ {
110
+ id: 'mock_monthly',
111
+ title: '1 Month',
112
+ price: '$9.99',
113
+ period: 'per month',
114
+ revenueCatPackage: null,
115
+ }
116
+ );
117
+ }
118
+
119
+ // Sort: Weekly, Annual, Monthly
120
+ mappedPlans.sort((a, b) => {
121
+ const order = ['1 Week', '1 Year', '1 Month'];
122
+ return order.indexOf(a.title) - order.indexOf(b.title);
123
+ });
124
+
125
+ return mappedPlans;
126
+ }, [offerings]);
127
+
128
+ useEffect(() => {
129
+ if (plans.length > 0 && !selectedPlan) {
130
+ // Select the Annual plan by default (80% OFF!)
131
+ const annualPlan = plans.find(p => p.title === '1 Year') || plans[0];
132
+ setSelectedPlan(annualPlan.id);
133
+ }
134
+ }, [plans, selectedPlan]);
135
+
136
+ const handlePurchase = async () => {
137
+ if (!selectedPlan) return;
138
+
139
+ const plan = plans.find(p => p.id === selectedPlan);
140
+ if (!plan || !plan.revenueCatPackage) {
141
+ Alert.alert('Error', 'Unable to process subscription. Please try again later.');
142
+ return;
143
+ }
144
+
145
+ try {
146
+ setIsLoading(true);
147
+ const result = await purchasePackage(plan.revenueCatPackage);
148
+ const success = result?.success;
149
+
150
+ if (success) {
151
+ Alert.alert(
152
+ 'Welcome to Premium!',
153
+ 'Your subscription is now active. You can now access all premium features!',
154
+ [
155
+ {
156
+ text: 'Start Using App',
157
+ onPress: async () => {
158
+ // Mark onboarding as completed when subscription is successful
159
+ await AsyncStorage.setItem('onboarding_completed', 'true');
160
+ logger.info('Subscription successful - onboarding marked as completed');
161
+
162
+ // Navigate to main app
163
+ router.replace('/(tabs)');
164
+ }
165
+ }
166
+ ]
167
+ );
168
+ } else {
169
+ Alert.alert('Error', 'Failed to activate subscription. Please try again.');
170
+ }
171
+ } catch (e: any) {
172
+ if (!e.userCancelled) {
173
+ console.error('Subscription activation error:', e);
174
+ Alert.alert('Error', 'Something went wrong. Please try again.');
175
+ }
176
+ } finally {
177
+ setIsLoading(false);
178
+ }
179
+ };
180
+
181
+ const handleSkip = async () => {
182
+ try {
183
+ // Mark onboarding as completed when user skips paywall
184
+ await AsyncStorage.setItem('onboarding_completed', 'true');
185
+ logger.info('Paywall skipped - onboarding marked as completed');
186
+
187
+ // Navigate to main app
188
+ router.replace('/(tabs)');
189
+ } catch (error) {
190
+ logger.error('Error completing onboarding after skip', { error });
191
+ Alert.alert('Error', 'Something went wrong. Please restart the app.');
192
+ }
193
+ };
194
+
195
+ const handleRestore = async () => {
196
+ try {
197
+ setIsLoading(true);
198
+ await restorePurchases();
199
+ Alert.alert('Success', 'Your purchases have been restored successfully.');
200
+ } catch (error) {
201
+ Alert.alert('Error', 'Failed to restore purchases. Please try again.');
202
+ } finally {
203
+ setIsLoading(false);
204
+ }
205
+ };
206
+
207
+ const renderPlanCard = (plan: PlanOption) => {
208
+ const isSelected = selectedPlan === plan.id;
209
+ const hasPopularBadge = plan.badge === 'Popular';
210
+ const hasDiscountBadge = plan.badge === '80% OFF!';
211
+
212
+ return (
213
+ <TouchableOpacity
214
+ key={plan.id}
215
+ style={[
216
+ styles.planCard,
217
+ hasPopularBadge && !isSelected && styles.popularPlan,
218
+ hasDiscountBadge && !isSelected && styles.discountPlan,
219
+ isSelected && styles.selectedPlan,
220
+ ]}
221
+ onPress={() => setSelectedPlan(plan.id)}
222
+ activeOpacity={0.7}
223
+ >
224
+ {plan.badge && (
225
+ <View style={[
226
+ styles.badge,
227
+ hasPopularBadge && styles.popularBadge,
228
+ hasDiscountBadge && styles.discountBadge,
229
+ ]}>
230
+ <Text style={styles.badgeText}>{plan.badge}</Text>
231
+ </View>
232
+ )}
233
+
234
+ <View style={styles.planContent}>
235
+ <Text style={styles.planTitle}>{plan.title}</Text>
236
+ <View style={styles.planPriceContainer}>
237
+ <Text style={styles.planPrice}>{plan.price}</Text>
238
+ {plan.pricePerWeek && (
239
+ <View style={styles.weeklyPriceContainer}>
240
+ <Text style={styles.pricePerWeek}>{plan.pricePerWeek}</Text>
241
+ <Text style={styles.perWeekLabel}>per week</Text>
242
+ </View>
243
+ )}
244
+ </View>
245
+ </View>
246
+ </TouchableOpacity>
247
+ );
248
+ };
249
+
250
+ // Image placeholder for children prop
251
+ const paywallContent = (
252
+ <View style={styles.imageContainer}>
253
+ <View style={styles.imagePlaceholder} />
254
+ </View>
255
+ );
256
+
257
+ // Middle content: subscription plans with optional warning banner
258
+ const middleContent = (
259
+ <View style={styles.middleContentContainer}>
260
+ {!hasOfferings && (
261
+ <View style={styles.warningBanner}>
262
+ <IconSymbol name="exclamationmark.triangle.fill" size={16} color="#F59E0B" />
263
+ <Text style={styles.warningText}>
264
+ Test Mode: Showing mock pricing. Configure RevenueCat to see real offerings.
265
+ </Text>
266
+ </View>
267
+ )}
268
+ <View style={styles.plansContainer}>
269
+ {plans.map(renderPlanCard)}
270
+ </View>
271
+ </View>
272
+ );
273
+
274
+ // Footer content: billing info and links
275
+ const footerContent = (
276
+ <>
277
+ <View style={styles.billingInfo}>
278
+ <View style={styles.billingRow}>
279
+ <IconSymbol name="checkmark" size={16} color="#4CAF50" />
280
+ <Text style={styles.billingText}>Recurring billing, cancel anytime</Text>
281
+ </View>
282
+ </View>
283
+
284
+ <View style={styles.footerLinks}>
285
+ <TouchableOpacity onPress={() => {}}>
286
+ <Text style={styles.footerLinkText}>Terms</Text>
287
+ </TouchableOpacity>
288
+ <TouchableOpacity onPress={() => {}}>
289
+ <Text style={styles.footerLinkText}>Privacy</Text>
290
+ </TouchableOpacity>
291
+ <TouchableOpacity onPress={handleRestore}>
292
+ <Text style={styles.footerLinkText}>Restore</Text>
293
+ </TouchableOpacity>
294
+ </View>
295
+ </>
296
+ );
297
+
298
+ // Loading content when subscription is loading
299
+ const loadingContent = subscriptionLoading ? (
300
+ <>
301
+ <ActivityIndicator size="large" color={Theme.colors.primary} />
302
+ <Text style={styles.loadingText}>Loading subscription details...</Text>
303
+ </>
304
+ ) : undefined;
305
+
306
+ // Show subscription management if already subscribed
307
+ if (hasActiveSubscription) {
308
+ const subscriptionContent = (
309
+ <View style={styles.imageContainer}>
310
+ <View style={styles.imagePlaceholder} />
311
+ <View style={styles.subscribedIconOverlay}>
312
+ <IconSymbol
313
+ name="checkmark.circle.fill"
314
+ size={80}
315
+ color="#4CAF50"
316
+ />
317
+ </View>
318
+ </View>
319
+ );
320
+
321
+ return (
322
+ <>
323
+ <Stack.Screen options={{ headerShown: false }} />
324
+ <OnboardingLayout
325
+ title="You're All Set!"
326
+ subtitle="You already have an active subscription. Enjoy all premium features!"
327
+ onContinue={async () => {
328
+ // Mark onboarding as completed for users with existing subscriptions
329
+ await AsyncStorage.setItem('onboarding_completed', 'true');
330
+ logger.info('Existing subscription detected - onboarding marked as completed');
331
+
332
+ // Navigate to main app
333
+ router.replace('/(tabs)');
334
+ }}
335
+ continueText="Continue to App"
336
+ pageIndicators={4}
337
+ totalPages={4}
338
+ >
339
+ {subscriptionContent}
340
+ </OnboardingLayout>
341
+ </>
342
+ );
343
+ }
344
+
345
+ return (
346
+ <>
347
+ <Stack.Screen options={{ headerShown: false }} />
348
+ <OnboardingLayout
349
+ title="Premium"
350
+ subtitle="Unlock all premium features and advanced functionality"
351
+ pageIndicators={4}
352
+ totalPages={4}
353
+ onContinue={handlePurchase}
354
+ continueDisabled={!selectedPlan || isLoading}
355
+ continueLoading={isLoading}
356
+ loadingContent={loadingContent}
357
+ middleContent={!subscriptionLoading ? middleContent : undefined}
358
+ footerContent={!subscriptionLoading ? footerContent : undefined}
359
+ onSkip={handleSkip}
360
+ showSkipAfter={3}
361
+ >
362
+ {paywallContent}
363
+ </OnboardingLayout>
364
+ </>
365
+ );
366
+ }
367
+
368
+ const imageSize = responsive.getImageContainerSize();
369
+ const planCardHeight = responsive.getPlanCardHeight();
370
+
371
+ const styles = StyleSheet.create({
372
+ // Image Section
373
+ imageContainer: {
374
+ width: imageSize.width,
375
+ height: imageSize.height,
376
+ justifyContent: 'center',
377
+ alignItems: 'center',
378
+ },
379
+ imagePlaceholder: {
380
+ width: '100%',
381
+ height: '100%',
382
+ backgroundColor: Theme.colors.backgroundSecondary,
383
+ borderRadius: responsive.scale(20),
384
+ borderWidth: 2,
385
+ borderColor: Theme.colors.borderLight,
386
+ borderStyle: 'dashed',
387
+ },
388
+
389
+ // Middle Content Container
390
+ middleContentContainer: {
391
+ width: '100%',
392
+ },
393
+
394
+ // Warning Banner
395
+ warningBanner: {
396
+ flexDirection: 'row',
397
+ alignItems: 'center',
398
+ backgroundColor: '#FEF3C7',
399
+ borderWidth: 1,
400
+ borderColor: '#F59E0B',
401
+ borderRadius: responsive.scale(8),
402
+ padding: responsive.scale(10),
403
+ marginBottom: getSpacing(12),
404
+ gap: getSpacing(8),
405
+ },
406
+ warningText: {
407
+ flex: 1,
408
+ fontSize: fontSize(12),
409
+ color: '#92400E',
410
+ fontWeight: '500',
411
+ },
412
+
413
+ // Plans Section - Horizontal Layout
414
+ plansContainer: {
415
+ flexDirection: 'row',
416
+ gap: responsive.scale(8),
417
+ },
418
+
419
+ planCard: {
420
+ flex: 1,
421
+ backgroundColor: '#F8F9FA',
422
+ borderRadius: responsive.scale(12),
423
+ padding: responsive.scale(10),
424
+ borderWidth: responsive.scale(1.5),
425
+ borderColor: '#E5E7EB',
426
+ position: 'relative',
427
+ alignItems: 'center',
428
+ height: planCardHeight,
429
+ justifyContent: 'center',
430
+ },
431
+ selectedPlan: {
432
+ borderColor: Theme.colors.primary,
433
+ backgroundColor: `${Theme.colors.primary}14`, // Light blue tint (14 = 8% opacity in hex)
434
+ borderWidth: responsive.scale(2.5),
435
+ shadowColor: Theme.colors.primary,
436
+ shadowOffset: { width: 0, height: 0 },
437
+ shadowOpacity: 0.3,
438
+ shadowRadius: responsive.scale(8),
439
+ elevation: 5,
440
+ },
441
+ popularPlan: {
442
+ borderColor: Theme.colors.primary,
443
+ },
444
+ discountPlan: {
445
+ borderColor: Theme.colors.primary,
446
+ },
447
+ badge: {
448
+ position: 'absolute',
449
+ top: responsive.scale(-8),
450
+ backgroundColor: Theme.colors.primary,
451
+ paddingHorizontal: getSpacing(8),
452
+ paddingVertical: getSpacing(3),
453
+ borderRadius: responsive.scale(8),
454
+ zIndex: 1,
455
+ },
456
+ popularBadge: {
457
+ backgroundColor: Theme.colors.primary,
458
+ },
459
+ discountBadge: {
460
+ backgroundColor: Theme.colors.primary,
461
+ },
462
+ badgeText: {
463
+ color: Theme.colors.textInverse,
464
+ fontSize: fontSize(10),
465
+ fontWeight: '700',
466
+ },
467
+ planContent: {
468
+ alignItems: 'center',
469
+ justifyContent: 'flex-start',
470
+ flex: 1,
471
+ paddingTop: getSpacing(12),
472
+ },
473
+ planTitle: {
474
+ fontSize: fontSize(14),
475
+ fontWeight: '600',
476
+ color: Theme.colors.text,
477
+ marginBottom: getSpacing(4),
478
+ },
479
+ planPriceContainer: {
480
+ alignItems: 'center',
481
+ },
482
+ planPrice: {
483
+ fontSize: fontSize(14),
484
+ fontWeight: '600',
485
+ color: Theme.colors.text,
486
+ },
487
+ weeklyPriceContainer: {
488
+ backgroundColor: Theme.colors.primary,
489
+ borderRadius: responsive.scale(6),
490
+ paddingHorizontal: getSpacing(8),
491
+ paddingVertical: getSpacing(4),
492
+ marginTop: getSpacing(8),
493
+ flexDirection: 'column',
494
+ alignItems: 'center',
495
+ },
496
+ pricePerWeek: {
497
+ fontSize: fontSize(12),
498
+ color: Theme.colors.textInverse,
499
+ fontWeight: '600',
500
+ },
501
+ perWeekLabel: {
502
+ fontSize: fontSize(11),
503
+ color: Theme.colors.textInverse,
504
+ fontWeight: '400',
505
+ },
506
+
507
+ // Billing Info
508
+ billingInfo: {
509
+ alignItems: 'center',
510
+ marginBottom: getSpacing(10),
511
+ },
512
+ billingRow: {
513
+ flexDirection: 'row',
514
+ alignItems: 'center',
515
+ gap: getSpacing(6),
516
+ },
517
+ billingText: {
518
+ fontSize: fontSize(13),
519
+ color: Theme.colors.textSecondary,
520
+ },
521
+
522
+ // Footer Links
523
+ footerLinks: {
524
+ flexDirection: 'row',
525
+ justifyContent: 'center',
526
+ gap: getSpacing(28),
527
+ },
528
+ footerLinkText: {
529
+ fontSize: fontSize(13),
530
+ color: Theme.colors.textSecondary,
531
+ textDecorationLine: 'underline',
532
+ },
533
+
534
+ // Loading state
535
+ loadingText: {
536
+ fontSize: fontSize(16),
537
+ color: Theme.colors.textSecondary,
538
+ marginTop: getSpacing(16),
539
+ textAlign: 'center',
540
+ },
541
+
542
+ // Subscribed state
543
+ subscribedIconOverlay: {
544
+ position: 'absolute',
545
+ justifyContent: 'center',
546
+ alignItems: 'center',
547
+ width: '100%',
548
+ height: '100%',
549
+ },
550
+ });