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,279 @@
1
+ import Redis from 'ioredis';
2
+ import { REDIS_KEYS, TTL } from '../lib/constants';
3
+
4
+ const redisHost = process.env.REDIS_HOST || 'localhost';
5
+ const redisPort = parseInt(process.env.REDIS_PORT || '6379', 10);
6
+ const redisPassword = process.env.REDIS_PASSWORD;
7
+ const redisDb = parseInt(process.env.REDIS_DB || '0', 10);
8
+ const isDevelopment = process.env.NODE_ENV === 'development';
9
+
10
+ // =============================================================================
11
+ // In-Memory Store (Development Fallback Only)
12
+ // =============================================================================
13
+ // This store is ONLY used when Redis is unavailable in development.
14
+ // In production, Redis unavailability will cause OAuth to fail (as intended).
15
+
16
+ interface MemoryStoreEntry {
17
+ data: string;
18
+ expiresAt: number;
19
+ }
20
+
21
+ class InMemoryStore {
22
+ private store = new Map<string, MemoryStoreEntry>();
23
+ private cleanupInterval: ReturnType<typeof setInterval> | null = null;
24
+
25
+ constructor() {
26
+ // Cleanup expired entries every 60 seconds
27
+ this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
28
+ }
29
+
30
+ async setex(key: string, ttlSeconds: number, value: string): Promise<void> {
31
+ this.store.set(key, {
32
+ data: value,
33
+ expiresAt: Date.now() + ttlSeconds * 1000,
34
+ });
35
+ }
36
+
37
+ async get(key: string): Promise<string | null> {
38
+ const entry = this.store.get(key);
39
+ if (!entry) return null;
40
+ if (Date.now() > entry.expiresAt) {
41
+ this.store.delete(key);
42
+ return null;
43
+ }
44
+ return entry.data;
45
+ }
46
+
47
+ async del(key: string): Promise<void> {
48
+ this.store.delete(key);
49
+ }
50
+
51
+ async getdel(key: string): Promise<string | null> {
52
+ const value = await this.get(key);
53
+ if (value) {
54
+ this.store.delete(key);
55
+ }
56
+ return value;
57
+ }
58
+
59
+ // Lua script simulation for atomic get-and-delete
60
+ async eval(_script: string, _numKeys: number, ...args: string[]): Promise<string | null> {
61
+ // For our use case, this is always the atomic get-delete pattern
62
+ const key = args[0];
63
+ return this.getdel(key);
64
+ }
65
+
66
+ private cleanup(): void {
67
+ const now = Date.now();
68
+ for (const [key, entry] of this.store.entries()) {
69
+ if (now > entry.expiresAt) {
70
+ this.store.delete(key);
71
+ }
72
+ }
73
+ }
74
+
75
+ destroy(): void {
76
+ if (this.cleanupInterval) {
77
+ clearInterval(this.cleanupInterval);
78
+ }
79
+ this.store.clear();
80
+ }
81
+ }
82
+
83
+ // =============================================================================
84
+ // Redis Client
85
+ // =============================================================================
86
+
87
+ let redis: Redis | null = null;
88
+ let redisAvailable = false;
89
+ let memoryStore: InMemoryStore | null = null;
90
+ let usingMemoryFallback = false;
91
+
92
+ function createRedisClient(): Redis {
93
+ const client = new Redis({
94
+ host: redisHost,
95
+ port: redisPort,
96
+ password: redisPassword || undefined,
97
+ db: redisDb,
98
+ maxRetriesPerRequest: 3,
99
+ retryStrategy(times) {
100
+ if (times > 3) return null; // Stop retrying after 3 attempts
101
+ return Math.min(times * 100, 3000); // Exponential backoff
102
+ },
103
+ enableOfflineQueue: false, // Don't queue commands when disconnected
104
+ });
105
+
106
+ client.on('error', (err) => {
107
+ console.error('Redis connection error:', err.message);
108
+ redisAvailable = false;
109
+
110
+ // In development, activate memory fallback when Redis fails
111
+ if (isDevelopment && !usingMemoryFallback) {
112
+ console.warn('⚠️ Redis unavailable in development - using in-memory store fallback');
113
+ console.warn('⚠️ OAuth state will NOT persist across server restarts!');
114
+ usingMemoryFallback = true;
115
+ if (!memoryStore) {
116
+ memoryStore = new InMemoryStore();
117
+ }
118
+ }
119
+ });
120
+
121
+ client.on('connect', () => {
122
+ console.log('Connected to Redis');
123
+ redisAvailable = true;
124
+ usingMemoryFallback = false;
125
+ });
126
+
127
+ client.on('close', () => {
128
+ redisAvailable = false;
129
+ });
130
+
131
+ return client;
132
+ }
133
+
134
+ // Initialize Redis client
135
+ export async function initRedis(): Promise<void> {
136
+ if (!redis) {
137
+ redis = createRedisClient();
138
+ try {
139
+ await redis.ping();
140
+ redisAvailable = true;
141
+ console.log('✅ Redis connected successfully');
142
+ } catch (err) {
143
+ console.error('Failed to connect to Redis:', (err as Error).message);
144
+ redisAvailable = false;
145
+
146
+ if (isDevelopment) {
147
+ console.warn('⚠️ Using in-memory store fallback for development');
148
+ usingMemoryFallback = true;
149
+ memoryStore = new InMemoryStore();
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ // Get the active store (Redis or in-memory fallback)
156
+ function getStore(): Redis | InMemoryStore {
157
+ if (redisAvailable && redis) {
158
+ return redis;
159
+ }
160
+
161
+ if (isDevelopment && usingMemoryFallback && memoryStore) {
162
+ return memoryStore;
163
+ }
164
+
165
+ throw new Error('Redis is not available');
166
+ }
167
+
168
+ // Check if Redis (or fallback) is available
169
+ export function isRedisAvailable(): boolean {
170
+ return redisAvailable || (isDevelopment && usingMemoryFallback);
171
+ }
172
+
173
+ // Check if using in-memory fallback
174
+ export function isUsingMemoryFallback(): boolean {
175
+ return usingMemoryFallback;
176
+ }
177
+
178
+ /**
179
+ * OAuth store helpers with graceful error handling
180
+ * Uses Redis when available, falls back to in-memory store in development
181
+ */
182
+ export const oauthStore = {
183
+ /**
184
+ * Store PKCE challenge during OAuth init
185
+ * @throws if store is unavailable (Redis down in production)
186
+ */
187
+ async storeOAuthState(
188
+ state: string,
189
+ data: {
190
+ code_challenge: string;
191
+ callback_url: string;
192
+ provider: string;
193
+ }
194
+ ): Promise<void> {
195
+ const store = getStore();
196
+ await store.setex(
197
+ `${REDIS_KEYS.OAUTH_WEB_STATE}${state}`,
198
+ TTL.OAUTH_STATE,
199
+ JSON.stringify({ ...data, created_at: Date.now() })
200
+ );
201
+ },
202
+
203
+ /**
204
+ * Retrieve OAuth state (does NOT delete - caller should delete after use)
205
+ */
206
+ async getOAuthState(state: string): Promise<{
207
+ code_challenge: string;
208
+ callback_url: string;
209
+ provider: string;
210
+ created_at: number;
211
+ } | null> {
212
+ const store = getStore();
213
+ const data = await store.get(`${REDIS_KEYS.OAUTH_WEB_STATE}${state}`);
214
+ if (!data) return null;
215
+ return JSON.parse(data);
216
+ },
217
+
218
+ /**
219
+ * Delete OAuth state after use
220
+ */
221
+ async deleteOAuthState(state: string): Promise<void> {
222
+ const store = getStore();
223
+ await store.del(`${REDIS_KEYS.OAUTH_WEB_STATE}${state}`);
224
+ },
225
+
226
+ /**
227
+ * Store exchange token after OAuth callback
228
+ */
229
+ async storeExchangeToken(
230
+ token: string,
231
+ data: {
232
+ session_token: string;
233
+ code_challenge: string;
234
+ }
235
+ ): Promise<void> {
236
+ const store = getStore();
237
+ await store.setex(
238
+ `${REDIS_KEYS.OAUTH_EXCHANGE}${token}`,
239
+ TTL.EXCHANGE_TOKEN,
240
+ JSON.stringify(data)
241
+ );
242
+ },
243
+
244
+ /**
245
+ * Retrieve and delete exchange token (single use)
246
+ * Uses atomic operations to prevent race conditions
247
+ */
248
+ async consumeExchangeToken(token: string): Promise<{
249
+ session_token: string;
250
+ code_challenge: string;
251
+ } | null> {
252
+ const store = getStore();
253
+ const key = `${REDIS_KEYS.OAUTH_EXCHANGE}${token}`;
254
+
255
+ // Use GETDEL for atomic get-and-delete (Redis 6.2+ / in-memory store)
256
+ let data: string | null = null;
257
+
258
+ try {
259
+ data = await store.getdel(key);
260
+ } catch {
261
+ // Fallback for older Redis versions using Lua script for atomicity
262
+ // This ensures the token can only be consumed once even under concurrent requests
263
+ const luaScript = `
264
+ local value = redis.call('GET', KEYS[1])
265
+ if value then
266
+ redis.call('DEL', KEYS[1])
267
+ end
268
+ return value
269
+ `;
270
+ data = (await store.eval(luaScript, 1, key)) as string | null;
271
+ }
272
+
273
+ if (!data) return null;
274
+ return JSON.parse(data);
275
+ },
276
+ };
277
+
278
+ // Export for direct Redis access if needed (use getStore() for fallback support)
279
+ export { redis, getStore };
@@ -0,0 +1,35 @@
1
+ # =============================================================================
2
+ # IMPORTANT: SDK API Keys Configuration
3
+ # =============================================================================
4
+ # SDK API keys (RevenueCat, Adjust, Scate) are configured in app.json and
5
+ # embedded at build time via expo-constants. They are NOT read from this file.
6
+ #
7
+ # This file is provided for reference and documentation purposes only.
8
+ # The actual keys used by the app come from app.json's "extra" field.
9
+ #
10
+ # To change SDK keys, update app.json and rebuild the app.
11
+ # =============================================================================
12
+
13
+ # Backend API
14
+ API_URL=http://localhost:8080
15
+
16
+ <% if (integrations.revenueCat.enabled) { %># RevenueCat Configuration
17
+ # NOTE: These keys are configured in app.json and embedded at build time
18
+ # Get your API keys from: https://app.revenuecat.com/
19
+ # Reference values (not used at runtime):
20
+ REVENUECAT_IOS_KEY=<%= integrations.revenueCat.iosKey || 'YOUR_IOS_API_KEY_HERE' %>
21
+ REVENUECAT_ANDROID_KEY=<%= integrations.revenueCat.androidKey || 'YOUR_ANDROID_API_KEY_HERE' %>
22
+ <% } %>
23
+ <% if (integrations.adjust.enabled) { %># Adjust Configuration
24
+ # NOTE: These values are configured in app.json and embedded at build time
25
+ # Get your app token from: https://dash.adjust.com/
26
+ # Reference values (not used at runtime):
27
+ ADJUST_APP_TOKEN=<%= integrations.adjust.appToken || 'YOUR_ADJUST_APP_TOKEN_HERE' %>
28
+ ADJUST_ENVIRONMENT=<%= integrations.adjust.environment %>
29
+ <% } %>
30
+ <% if (integrations.scate.enabled) { %># Scate Configuration
31
+ # NOTE: This key is configured in app.json and embedded at build time
32
+ # Get your API key from: https://scate.io/
33
+ # Reference value (not used at runtime):
34
+ SCATE_API_KEY=<%= integrations.scate.apiKey || 'YOUR_SCATE_API_KEY_HERE' %>
35
+ <% } %>
@@ -0,0 +1,167 @@
1
+ # React Native/Expo Mobile App - Git Ignore
2
+
3
+ # Dependencies
4
+ node_modules/
5
+ npm-debug.log*
6
+ yarn-debug.log*
7
+ yarn-error.log*
8
+ .pnpm-store/
9
+
10
+ # Expo
11
+ .expo/
12
+ dist/
13
+ web-build/
14
+ expo-env.d.ts
15
+
16
+ # Native build directories and files
17
+ ios/
18
+ android/
19
+ .kotlin/
20
+
21
+ # Metro
22
+ .metro-health-check*
23
+ .metro/
24
+
25
+ # Debug files
26
+ npm-debug.*
27
+ yarn-debug.*
28
+ yarn-error.*
29
+
30
+ # macOS
31
+ .DS_Store
32
+ *.pem
33
+
34
+ # Environment files
35
+ .env
36
+ .env.*
37
+ !.env.example
38
+
39
+ # TypeScript
40
+ *.tsbuildinfo
41
+ tsconfig.tsbuildinfo
42
+
43
+ # EAS
44
+ build/
45
+ *.ipa
46
+ *.apk
47
+ *.aab
48
+ .eas/
49
+
50
+ # React Native
51
+ *.orig.*
52
+ *.jks
53
+ *.p8
54
+ *.p12
55
+ *.key
56
+ *.mobileprovision
57
+ *.keystore
58
+
59
+ # Temporary files
60
+ tmp/
61
+ temp/
62
+ *.tmp
63
+ *.temp
64
+
65
+ # Logs
66
+ logs/
67
+ *.log
68
+
69
+ # Test coverage
70
+ coverage/
71
+ .nyc_output/
72
+
73
+ # Editor and IDE files
74
+ .vscode/
75
+ .idea/
76
+ *.swp
77
+ *.swo
78
+ *~
79
+
80
+ # Cache directories
81
+ .cache/
82
+ .eslintcache
83
+
84
+ # OS files
85
+ .DS_Store
86
+ .DS_Store?
87
+ ._*
88
+ .Spotlight-V100
89
+ .Trashes
90
+ ehthumbs.db
91
+ Thumbs.db
92
+
93
+ # Backup files
94
+ *.bak
95
+ *.backup
96
+ *.old
97
+
98
+ # Archive files
99
+ *.zip
100
+ *.tar.gz
101
+ *.rar
102
+
103
+ # Simulator files
104
+ *.trace
105
+
106
+ # Flipper
107
+ Flipper/
108
+
109
+ # CocoaPods (iOS)
110
+ ios/Pods/
111
+ ios/Podfile.lock
112
+ *.xcworkspace
113
+
114
+ # Gradle (Android)
115
+ android/.gradle/
116
+ android/build/
117
+ android/app/build/
118
+ android/local.properties
119
+
120
+ # Fastlane
121
+ fastlane/report.xml
122
+ fastlane/Preview.html
123
+ fastlane/screenshots
124
+ fastlane/test_output
125
+ fastlane/readme.md
126
+
127
+ # Bundle artifacts
128
+ *.jsbundle
129
+
130
+ # Watchman
131
+ .watchmanconfig
132
+
133
+ # Xcode
134
+ *.xcodeproj/*
135
+ !*.xcodeproj/project.pbxproj
136
+ !*.xcodeproj/xcshareddata/
137
+ !*.xcodeproj/project.xcworkspace/
138
+ *.xcworkspace/*
139
+ !*.xcworkspace/contents.xcworkspacedata
140
+ /*.gcno
141
+
142
+ # Android Studio
143
+ .gradle
144
+ /build/
145
+ /captures
146
+ .externalNativeBuild
147
+
148
+ # Detox
149
+ e2e/*.js
150
+ e2e/artifacts/
151
+
152
+ # Storybook
153
+ storybook-static/
154
+
155
+ # Firebase
156
+ .firebase/
157
+ firebase-debug.log
158
+ firebase-debug.*.log
159
+
160
+ # React Native Debugger
161
+ rndebugger_log.txt
162
+
163
+ # CodePush
164
+ CodePush/
165
+
166
+ # App Center
167
+ AppCenter/
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import { router } from 'expo-router';
4
+ import { Button } from '../src/components/ui';
5
+
6
+ export default function NotFoundScreen() {
7
+ const handleGoHome = () => {
8
+ router.replace('/(tabs)');
9
+ };
10
+
11
+ const handleGoBack = () => {
12
+ if (router.canGoBack()) {
13
+ router.back();
14
+ } else {
15
+ handleGoHome();
16
+ }
17
+ };
18
+
19
+ return (
20
+ <View style={styles.container}>
21
+ <Text style={styles.title}>404</Text>
22
+ <Text style={styles.message}>Page Not Found</Text>
23
+ <Text style={styles.description}>
24
+ The page you're looking for doesn't exist or has been moved.
25
+ </Text>
26
+
27
+ <View style={styles.actions}>
28
+ <Button
29
+ title="Go Back"
30
+ onPress={handleGoBack}
31
+ style={styles.button}
32
+ />
33
+ <Button
34
+ title="Go Home"
35
+ variant="outline"
36
+ onPress={handleGoHome}
37
+ style={styles.button}
38
+ />
39
+ </View>
40
+ </View>
41
+ );
42
+ }
43
+
44
+ const styles = StyleSheet.create({
45
+ container: {
46
+ flex: 1,
47
+ justifyContent: 'center',
48
+ alignItems: 'center',
49
+ padding: 20,
50
+ backgroundColor: '#FFFFFF',
51
+ },
52
+
53
+ title: {
54
+ fontSize: 72,
55
+ fontWeight: 'bold',
56
+ color: '#007AFF',
57
+ marginBottom: 16,
58
+ },
59
+
60
+ message: {
61
+ fontSize: 24,
62
+ fontWeight: '600',
63
+ color: '#000000',
64
+ marginBottom: 8,
65
+ textAlign: 'center',
66
+ },
67
+
68
+ description: {
69
+ fontSize: 16,
70
+ color: '#8E8E93',
71
+ textAlign: 'center',
72
+ lineHeight: 22,
73
+ marginBottom: 32,
74
+ },
75
+
76
+ actions: {
77
+ gap: 12,
78
+ width: '100%',
79
+ maxWidth: 300,
80
+ },
81
+
82
+ button: {
83
+ width: '100%',
84
+ },
85
+ });
@@ -0,0 +1,71 @@
1
+ import { Stack, router } from 'expo-router';
2
+ import { useEffect<% if (features.sessionManagement) { %>, useState<% } %> } from 'react';
3
+ import Constants from 'expo-constants';
4
+ import { ThemeProvider } from '@/context/ThemeContext';
5
+ <% if (features.onboarding.enabled) { %>import AsyncStorage from '@react-native-async-storage/async-storage';
6
+ <% } %><% if (integrations.revenueCat.enabled || integrations.adjust.enabled || integrations.scate.enabled) { %>import { initializeSDKs } from '@/services/sdkInitializer';
7
+ <% } %><% if (features.sessionManagement) { %>import { useSessionActions, useSession } from '../src/store/deviceSession.store';
8
+ <% } %>import { logger } from '../src/utils/logger';
9
+
10
+ export default function RootLayout() {
11
+ <% if (features.sessionManagement) { %> const { initializeSession } = useSessionActions();
12
+ const { isSessionChecked } = useSession();
13
+ <% } else { %> const [isReady, setIsReady] = useState(false);
14
+ <% } %>
15
+ // Get feature flags from app.json
16
+ const onboardingEnabled = Constants.expoConfig?.extra?.features?.onboarding?.enabled ?? false;
17
+
18
+ useEffect(() => {
19
+ <% if (integrations.revenueCat.enabled || integrations.adjust.enabled || integrations.scate.enabled) { %> // Initialize SDKs
20
+ initializeSDKs().catch(console.error);
21
+
22
+ <% } %> // App initialization and navigation
23
+ const initializeApp = async () => {
24
+ <% if (features.sessionManagement) { %> // Skip if session already checked
25
+ if (isSessionChecked) {
26
+ logger.debug('RootLayout: Session already checked, skipping');
27
+ return;
28
+ }
29
+ <% } %>
30
+ try {
31
+ logger.info('RootLayout: Initializing app...');
32
+
33
+ <% if (features.onboarding.enabled) { %> // Check onboarding completion
34
+ const onboardingCompleted = await AsyncStorage.getItem('onboarding_completed');
35
+
36
+ if (onboardingEnabled && onboardingCompleted !== 'true') {
37
+ // Onboarding not completed - navigate to onboarding
38
+ logger.info('RootLayout: Onboarding not completed, navigating to onboarding');
39
+ router.replace('/(onboarding)/page-1');
40
+ return;
41
+ }
42
+ <% } %>
43
+ <% if (features.sessionManagement) { %> // Initialize session
44
+ logger.info('RootLayout: Initializing session');
45
+ await initializeSession();
46
+ <% } %>
47
+ router.replace('/(tabs)');
48
+ } catch (error) {
49
+ logger.error('RootLayout: Failed to initialize app', { error });
50
+ // Fallback to tabs on error
51
+ router.replace('/(tabs)');
52
+ }<% if (!features.sessionManagement) { %> finally {
53
+ setIsReady(true);
54
+ }<% } %>
55
+ };
56
+
57
+ initializeApp();
58
+ }, [<% if (features.sessionManagement) { %>isSessionChecked, initializeSession, <% } %>onboardingEnabled]);
59
+
60
+ return (
61
+ <ThemeProvider>
62
+ <Stack screenOptions={{ headerShown: false }}>
63
+ <% if (features.onboarding.enabled) { %> <Stack.Screen name="(onboarding)" />
64
+ <% } %><% if (features.authentication.enabled) { %> <Stack.Screen name="(auth)" />
65
+ <% } %><% if (features.paywall) { %> <Stack.Screen name="paywall" options={{ presentation: 'modal' }} />
66
+ <% } %> <Stack.Screen name="(tabs)" />
67
+ <Stack.Screen name="+not-found" />
68
+ </Stack>
69
+ </ThemeProvider>
70
+ );
71
+ }