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,51 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+ import { expoClient } from "@better-auth/expo/client";
3
+ import * as SecureStore from "expo-secure-store";
4
+ import Constants from "expo-constants";
5
+
6
+ // Get API URL from Expo config or default
7
+ const API_URL = Constants.expoConfig?.extra?.apiUrl || "http://localhost:8080";
8
+
9
+ // Get app scheme from Expo config (defined at expo.scheme in app.json)
10
+ const APP_SCHEME = Constants.expoConfig?.scheme || "<%= appScheme %>";
11
+
12
+ /**
13
+ * BetterAuth client configured for Expo/React Native
14
+ *
15
+ * Features:
16
+ * - Automatic OAuth deep link handling (callbackURL paths become deep links)
17
+ * - Secure session storage via expo-secure-store
18
+ * - Session caching to avoid loading spinners on app reload
19
+ */
20
+ export const authClient = createAuthClient({
21
+ baseURL: API_URL,
22
+ plugins: [
23
+ expoClient({
24
+ scheme: APP_SCHEME, // Must match app.json scheme
25
+ storagePrefix: APP_SCHEME, // Prefix for secure storage keys
26
+ storage: SecureStore, // Use secure storage (not AsyncStorage!)
27
+ }),
28
+ ],
29
+ });
30
+
31
+ // Export commonly used methods and hooks
32
+ export const {
33
+ signIn,
34
+ signUp,
35
+ signOut,
36
+ useSession,
37
+ getSession,
38
+ } = authClient;
39
+
40
+ /**
41
+ * Get cookies for authenticated API requests
42
+ * Use this when making custom API calls that need authentication
43
+ *
44
+ * @example
45
+ * const cookies = getCookies();
46
+ * const response = await fetch(url, {
47
+ * headers: { "Cookie": cookies || "" },
48
+ * credentials: "omit"
49
+ * });
50
+ */
51
+ export const getCookies = () => authClient.getCookie();
@@ -0,0 +1,71 @@
1
+ import axios from 'axios';
2
+ import AsyncStorage from '@react-native-async-storage/async-storage';
3
+ import Constants from 'expo-constants';
4
+ <% if (features.authentication.enabled) { %>
5
+ import { authClient } from '../lib/auth-client';
6
+ <% } %>
7
+
8
+ // Storage key for device sessions (anonymous users)
9
+ const DEVICE_SESSION_TOKEN_KEY = 'device_session_token';
10
+
11
+ // API Configuration
12
+ const API_URL = Constants.expoConfig?.extra?.apiUrl || 'http://localhost:8080';
13
+
14
+ const api = axios.create({
15
+ baseURL: API_URL,
16
+ timeout: 10000, // 10 second timeout
17
+ headers: {
18
+ 'Content-Type': 'application/json',
19
+ },
20
+ });
21
+
22
+ // Request interceptor to add auth cookies and device session token
23
+ api.interceptors.request.use(
24
+ async (config) => {
25
+ try {
26
+ <% if (features.authentication.enabled) { %>
27
+ // Add BetterAuth session cookies for authenticated requests
28
+ const cookies = authClient.getCookie();
29
+ if (cookies) {
30
+ config.headers.Cookie = cookies;
31
+ }
32
+ <% } %>
33
+
34
+ // Add device session token if available (for anonymous sessions)
35
+ const deviceSessionToken = await AsyncStorage.getItem(DEVICE_SESSION_TOKEN_KEY);
36
+ if (deviceSessionToken) {
37
+ config.headers['X-Device-Session-Token'] = deviceSessionToken;
38
+ }
39
+
40
+ return config;
41
+ } catch (error) {
42
+ console.error('API Request Error:', error);
43
+ return config;
44
+ }
45
+ },
46
+ (error) => {
47
+ console.error('API Request Setup Error:', error);
48
+ return Promise.reject(error);
49
+ }
50
+ );
51
+
52
+ // Response interceptor for handling common responses and errors
53
+ api.interceptors.response.use(
54
+ (response) => response,
55
+ async (error) => {
56
+ // Handle 401 errors - session expired or invalid
57
+ if (error.response?.status === 401) {
58
+ <% if (features.authentication.enabled) { %>
59
+ // Sign out to clear BetterAuth session
60
+ try {
61
+ await authClient.signOut();
62
+ } catch {
63
+ // Ignore sign out errors
64
+ }
65
+ <% } %>
66
+ }
67
+ return Promise.reject(error);
68
+ }
69
+ );
70
+
71
+ export default api;
@@ -0,0 +1,179 @@
1
+ import axios from 'axios';
2
+
3
+ export interface AppError {
4
+ type: string;
5
+ message: string;
6
+ details?: any;
7
+ }
8
+
9
+ class ErrorService {
10
+ private static instance: ErrorService;
11
+
12
+ private constructor() {}
13
+
14
+ public static getInstance(): ErrorService {
15
+ if (!ErrorService.instance) {
16
+ ErrorService.instance = new ErrorService();
17
+ }
18
+ return ErrorService.instance;
19
+ }
20
+
21
+ handleAxiosError(error: any, context?: { [key: string]: any }): AppError {
22
+ if (axios.isAxiosError(error)) {
23
+ // Network error (no response)
24
+ if (!error.response) {
25
+ if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
26
+ return {
27
+ type: 'TIMEOUT_ERROR',
28
+ message: 'Request timeout. Please try again.',
29
+ details: { context }
30
+ };
31
+ }
32
+ return {
33
+ type: 'CONNECTION_ERROR',
34
+ message: 'Unable to connect. Please check your internet connection.',
35
+ details: { context }
36
+ };
37
+ }
38
+
39
+ // Server responded with error status
40
+ const status = error.response.status;
41
+ const data = error.response.data;
42
+
43
+ // Handle structured API errors
44
+ if (data?.error?.code && data?.error?.message) {
45
+ return {
46
+ type: data.error.code,
47
+ message: data.error.message,
48
+ details: data.error.details || context
49
+ };
50
+ }
51
+
52
+ // Handle common HTTP status codes
53
+ switch (status) {
54
+ case 400:
55
+ return {
56
+ type: 'VALIDATION_ERROR',
57
+ message: data?.message || 'Invalid request data.',
58
+ details: data?.details || context
59
+ };
60
+ case 401:
61
+ return {
62
+ type: 'AUTH_ERROR',
63
+ message: 'Please log in to continue.',
64
+ details: context
65
+ };
66
+ case 403:
67
+ return {
68
+ type: 'PERMISSION_ERROR',
69
+ message: 'You do not have permission to perform this action.',
70
+ details: context
71
+ };
72
+ case 404:
73
+ return {
74
+ type: 'NOT_FOUND_ERROR',
75
+ message: 'The requested resource was not found.',
76
+ details: context
77
+ };
78
+ case 409:
79
+ return {
80
+ type: 'CONFLICT_ERROR',
81
+ message: data?.message || 'A conflict occurred.',
82
+ details: context
83
+ };
84
+ case 429:
85
+ return {
86
+ type: 'RATE_LIMIT_ERROR',
87
+ message: 'Too many requests. Please try again later.',
88
+ details: context
89
+ };
90
+ case 500:
91
+ return {
92
+ type: 'SERVER_ERROR',
93
+ message: 'An internal server error occurred.',
94
+ details: context
95
+ };
96
+ case 503:
97
+ return {
98
+ type: 'SERVICE_UNAVAILABLE',
99
+ message: 'The service is temporarily unavailable.',
100
+ details: context
101
+ };
102
+ default:
103
+ return {
104
+ type: 'HTTP_ERROR',
105
+ message: data?.message || `Request failed with status ${status}`,
106
+ details: { status, context }
107
+ };
108
+ }
109
+ }
110
+
111
+ return this.handleGenericError(error, context);
112
+ }
113
+
114
+ handleGenericError(error: any, context?: { [key: string]: any }): AppError {
115
+ if (error instanceof Error) {
116
+ return {
117
+ type: 'GENERIC_ERROR',
118
+ message: error.message,
119
+ details: { context }
120
+ };
121
+ }
122
+
123
+ return {
124
+ type: 'UNKNOWN_ERROR',
125
+ message: 'An unexpected error occurred.',
126
+ details: { originalError: String(error), context }
127
+ };
128
+ }
129
+
130
+ getUserFriendlyMessage(error: AppError): string {
131
+ const suggestions: { [key: string]: string } = {
132
+ CONNECTION_ERROR: 'Please check your internet connection and try again.',
133
+ TIMEOUT_ERROR: 'The request is taking longer than usual. Please try again.',
134
+ AUTH_ERROR: 'Please log in again.',
135
+ PERMISSION_ERROR: 'You do not have permission to perform this action.',
136
+ VALIDATION_ERROR: 'Please check your input and try again.',
137
+ CONFLICT_ERROR: 'This action conflicts with existing data.',
138
+ RATE_LIMIT_ERROR: 'Please wait a moment before trying again.',
139
+ SERVER_ERROR: 'Something went wrong on our end. Please try again later.',
140
+ SERVICE_UNAVAILABLE: 'The service is temporarily unavailable. Please try again later.',
141
+ };
142
+
143
+ return suggestions[error.type] || error.message;
144
+ }
145
+
146
+ getSuggestions(error: AppError): string[] {
147
+ const suggestionMap: { [key: string]: string[] } = {
148
+ CONNECTION_ERROR: [
149
+ 'Check your internet connection',
150
+ 'Try switching between Wi-Fi and cellular data',
151
+ 'Move to an area with better signal'
152
+ ],
153
+ TIMEOUT_ERROR: [
154
+ 'Try again in a few moments',
155
+ 'Check your internet connection',
156
+ 'The server might be experiencing high load'
157
+ ],
158
+ AUTH_ERROR: [
159
+ 'Please log in again',
160
+ 'Check if your session has expired',
161
+ 'Try logging out and back in'
162
+ ],
163
+ VALIDATION_ERROR: [
164
+ 'Double-check your input',
165
+ 'Make sure all required fields are filled',
166
+ 'Check the format of your data'
167
+ ],
168
+ SERVER_ERROR: [
169
+ 'Try again in a few minutes',
170
+ 'Contact support if the problem persists',
171
+ 'Check if there are any ongoing service issues'
172
+ ]
173
+ };
174
+
175
+ return suggestionMap[error.type] || ['Please try again', 'Contact support if the problem persists'];
176
+ }
177
+ }
178
+
179
+ export const errorService = ErrorService.getInstance();
@@ -0,0 +1,36 @@
1
+ <% if (integrations.att.enabled) { %>import { Platform } from 'react-native';
2
+ import { useATTStore } from '@/store/att.store';
3
+ <% } %><% if (integrations.revenueCat.enabled) { %>import { revenueCatService } from './revenuecatService';
4
+ <% } %><% if (integrations.adjust.enabled) { %>import { adjustService } from './adjustService';
5
+ <% } %><% if (integrations.scate.enabled) { %>import { scateService } from './scateService';
6
+ <% } %>
7
+ export const initializeSDKs = async () => {
8
+ try {
9
+ <% if (integrations.revenueCat.enabled) { %> // Initialize RevenueCat
10
+ revenueCatService.initialize();
11
+ <% } %>
12
+ <% if (integrations.adjust.enabled) { %> // Initialize Adjust
13
+ <% if (integrations.att.enabled) { %> // Handle ATT permissions on iOS first
14
+ if (Platform.OS === 'ios') {
15
+ const attStore = useATTStore.getState();
16
+ await attStore.initialize();
17
+ await attStore.requestPermissions();
18
+ }
19
+ <% } %> adjustService.initialize();
20
+ <% } %>
21
+ <% if (integrations.scate.enabled) { %> // Initialize Scate
22
+ scateService.initialize();
23
+ <% } %>
24
+ <% if (integrations.adjust.enabled && (integrations.revenueCat.enabled || integrations.scate.enabled)) { %>
25
+ // Distribute ADID to other SDKs
26
+ const adid = await adjustService.getAdid();
27
+ if (adid) {
28
+ <% if (integrations.revenueCat.enabled) { %> // Set Adjust ID in RevenueCat
29
+ revenueCatService.setAdjustId(adid);
30
+ <% } %><% if (integrations.scate.enabled) { %> // Set ADID in Scate
31
+ scateService.setAdid(adid);
32
+ <% } %> }
33
+ <% } %> } catch (error) {
34
+ console.error('SDK initialization error:', error);
35
+ }
36
+ };
@@ -0,0 +1,18 @@
1
+ // UI Store (always included)
2
+ export * from './ui.store';
3
+
4
+ <% if (features.sessionManagement) { %>// Device Session Store
5
+ export * from './deviceSession.store';
6
+ <% } %>
7
+ <% if (integrations.revenueCat.enabled) { %>// RevenueCat Store
8
+ export * from './revenuecat.store';
9
+ <% } %>
10
+ <% if (integrations.adjust.enabled) { %>// Adjust Store
11
+ export * from './adjust.store';
12
+ <% } %>
13
+ <% if (integrations.scate.enabled) { %>// Scate Store
14
+ export * from './scate.store';
15
+ <% } %>
16
+ <% if (integrations.att.enabled) { %>// ATT Store
17
+ export * from './att.store';
18
+ <% } %>
@@ -0,0 +1,100 @@
1
+ import { create } from 'zustand';
2
+
3
+ interface UIState {
4
+ // Global loading states
5
+ isAppLoading: boolean;
6
+ isNetworkLoading: boolean;
7
+
8
+ // Theme
9
+ colorScheme: 'light' | 'dark' | 'system';
10
+
11
+ // Notifications/Toasts
12
+ notification: {
13
+ id: string;
14
+ type: 'success' | 'error' | 'warning' | 'info';
15
+ title: string;
16
+ message?: string;
17
+ duration?: number;
18
+ } | null;
19
+
20
+ // Actions
21
+ setAppLoading: (loading: boolean) => void;
22
+ setNetworkLoading: (loading: boolean) => void;
23
+ setColorScheme: (scheme: 'light' | 'dark' | 'system') => void;
24
+ showNotification: (notification: {
25
+ type: 'success' | 'error' | 'warning' | 'info';
26
+ title: string;
27
+ message?: string;
28
+ duration?: number;
29
+ }) => void;
30
+ hideNotification: () => void;
31
+
32
+ // Computed
33
+ isAnyLoading: boolean;
34
+ }
35
+
36
+ export const useUIStore = create<UIState>((set, get) => ({
37
+ // Initial state
38
+ isAppLoading: false,
39
+ isNetworkLoading: false,
40
+ colorScheme: 'system',
41
+ notification: null,
42
+
43
+ // Actions
44
+ setAppLoading: (isAppLoading) => set({ isAppLoading }),
45
+ setNetworkLoading: (isNetworkLoading) => set({ isNetworkLoading }),
46
+ setColorScheme: (colorScheme) => set({ colorScheme }),
47
+
48
+ showNotification: (notificationData) => {
49
+ const id = `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
50
+ set({
51
+ notification: {
52
+ id,
53
+ ...notificationData,
54
+ duration: notificationData.duration ?? 4000,
55
+ }
56
+ });
57
+
58
+ // Auto-hide after duration
59
+ const duration = notificationData.duration ?? 4000;
60
+ if (duration > 0) {
61
+ setTimeout(() => {
62
+ const currentNotification = get().notification;
63
+ if (currentNotification?.id === id) {
64
+ set({ notification: null });
65
+ }
66
+ }, duration);
67
+ }
68
+ },
69
+
70
+ hideNotification: () => set({ notification: null }),
71
+
72
+ // Computed values
73
+ get isAnyLoading() {
74
+ const state = get();
75
+ return state.isAppLoading || state.isNetworkLoading;
76
+ },
77
+ }));
78
+
79
+ // Selectors for commonly used UI state
80
+ export const useUI = () => {
81
+ const state = useUIStore();
82
+ return {
83
+ isAppLoading: state.isAppLoading,
84
+ isNetworkLoading: state.isNetworkLoading,
85
+ isAnyLoading: state.isAnyLoading,
86
+ colorScheme: state.colorScheme,
87
+ notification: state.notification,
88
+ };
89
+ };
90
+
91
+ export const useUIActions = () => {
92
+ const state = useUIStore();
93
+ return {
94
+ setAppLoading: state.setAppLoading,
95
+ setNetworkLoading: state.setNetworkLoading,
96
+ setColorScheme: state.setColorScheme,
97
+ showNotification: state.showNotification,
98
+ hideNotification: state.hideNotification,
99
+ };
100
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Format date strings for display
3
+ */
4
+ export const formatDate = (dateString: string, options?: Intl.DateTimeFormatOptions): string => {
5
+ try {
6
+ const date = new Date(dateString);
7
+ if (isNaN(date.getTime())) {
8
+ return dateString; // Return original if invalid
9
+ }
10
+
11
+ const defaultOptions: Intl.DateTimeFormatOptions = {
12
+ year: 'numeric',
13
+ month: 'short',
14
+ day: 'numeric',
15
+ };
16
+
17
+ return date.toLocaleDateString(undefined, options || defaultOptions);
18
+ } catch (error) {
19
+ return dateString; // Return original if formatting fails
20
+ }
21
+ };
22
+
23
+ /**
24
+ * Format relative time (e.g., "2 hours ago")
25
+ */
26
+ export const formatRelativeTime = (dateString: string): string => {
27
+ try {
28
+ const date = new Date(dateString);
29
+ const now = new Date();
30
+ const diffInSeconds = (now.getTime() - date.getTime()) / 1000;
31
+
32
+ if (diffInSeconds < 60) {
33
+ return 'Just now';
34
+ }
35
+
36
+ const diffInMinutes = Math.floor(diffInSeconds / 60);
37
+ if (diffInMinutes < 60) {
38
+ return `${diffInMinutes} ${diffInMinutes === 1 ? 'minute' : 'minutes'} ago`;
39
+ }
40
+
41
+ const diffInHours = Math.floor(diffInMinutes / 60);
42
+ if (diffInHours < 24) {
43
+ return `${diffInHours} ${diffInHours === 1 ? 'hour' : 'hours'} ago`;
44
+ }
45
+
46
+ const diffInDays = Math.floor(diffInHours / 24);
47
+ if (diffInDays < 7) {
48
+ return `${diffInDays} ${diffInDays === 1 ? 'day' : 'days'} ago`;
49
+ }
50
+
51
+ // For longer periods, show the actual date
52
+ return formatDate(dateString);
53
+ } catch (error) {
54
+ return dateString;
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Capitalize first letter of each word
60
+ */
61
+ export const capitalizeWords = (str: string): string => {
62
+ return str.replace(/\b\w/g, l => l.toUpperCase());
63
+ };
64
+
65
+ /**
66
+ * Truncate text with ellipsis
67
+ */
68
+ export const truncateText = (text: string, maxLength: number): string => {
69
+ if (text.length <= maxLength) return text;
70
+ return text.slice(0, maxLength - 3) + '...';
71
+ };
72
+
73
+ /**
74
+ * Format email for display (hide middle part)
75
+ */
76
+ export const formatEmailForDisplay = (email: string): string => {
77
+ const [localPart, domain] = email.split('@');
78
+ if (!domain) return email;
79
+
80
+ if (localPart.length <= 2) return email;
81
+
82
+ const visibleChars = Math.max(1, Math.floor(localPart.length * 0.3));
83
+ const hiddenPart = '*'.repeat(localPart.length - visibleChars * 2);
84
+ const maskedLocal = localPart.slice(0, visibleChars) + hiddenPart + localPart.slice(-visibleChars);
85
+
86
+ return `${maskedLocal}@${domain}`;
87
+ };
88
+
89
+ /**
90
+ * Validate email format
91
+ */
92
+ export const isValidEmail = (email: string): boolean => {
93
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
94
+ return emailRegex.test(email);
95
+ };
96
+
97
+ /**
98
+ * Format name for display
99
+ */
100
+ export const formatDisplayName = (name: string | null | undefined, fallback: string = 'User'): string => {
101
+ if (!name || !name.trim()) {
102
+ return fallback;
103
+ }
104
+ return name.trim();
105
+ };
@@ -0,0 +1,73 @@
1
+ interface LogLevel {
2
+ DEBUG: number;
3
+ INFO: number;
4
+ WARN: number;
5
+ ERROR: number;
6
+ }
7
+
8
+ const LOG_LEVELS: LogLevel = {
9
+ DEBUG: 0,
10
+ INFO: 1,
11
+ WARN: 2,
12
+ ERROR: 3,
13
+ };
14
+
15
+ class Logger {
16
+ private currentLevel: number;
17
+
18
+ constructor() {
19
+ // Set log level based on environment
20
+ this.currentLevel = __DEV__ ? LOG_LEVELS.DEBUG : LOG_LEVELS.WARN;
21
+ }
22
+
23
+ private shouldLog(level: number): boolean {
24
+ return level >= this.currentLevel;
25
+ }
26
+
27
+ private formatMessage(level: string, message: string, data?: any): string {
28
+ const timestamp = new Date().toISOString();
29
+ const prefix = `[${timestamp}] [${level}]`;
30
+
31
+ if (data) {
32
+ return `${prefix} ${message}\n${JSON.stringify(data, null, 2)}`;
33
+ }
34
+
35
+ return `${prefix} ${message}`;
36
+ }
37
+
38
+ debug(message: string, data?: any): void {
39
+ if (this.shouldLog(LOG_LEVELS.DEBUG)) {
40
+ console.log(this.formatMessage('DEBUG', message, data));
41
+ }
42
+ }
43
+
44
+ info(message: string, data?: any): void {
45
+ if (this.shouldLog(LOG_LEVELS.INFO)) {
46
+ console.info(this.formatMessage('INFO', message, data));
47
+ }
48
+ }
49
+
50
+ warn(message: string, data?: any): void {
51
+ if (this.shouldLog(LOG_LEVELS.WARN)) {
52
+ console.warn(this.formatMessage('WARN', message, data));
53
+ }
54
+ }
55
+
56
+ error(message: string, data?: any): void {
57
+ if (this.shouldLog(LOG_LEVELS.ERROR)) {
58
+ console.error(this.formatMessage('ERROR', message, data));
59
+ }
60
+ }
61
+
62
+ // Convenience method for API logging
63
+ apiRequest(method: string, url: string, data?: any): void {
64
+ this.debug(`API ${method.toUpperCase()} ${url}`, data);
65
+ }
66
+
67
+ apiResponse(method: string, url: string, status: number, data?: any): void {
68
+ const level = status >= 400 ? 'error' : 'debug';
69
+ this[level](`API ${method.toUpperCase()} ${url} - ${status}`, data);
70
+ }
71
+ }
72
+
73
+ export const logger = new Logger();