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,84 @@
1
+ import { trackingPermissions, TrackingPermissionResult } from './trackingPermissions';
2
+ import { logger } from '../utils/logger';
3
+
4
+ class ATTService {
5
+ /**
6
+ * Initialize ATT permissions service
7
+ */
8
+ async initialize(): Promise<TrackingPermissionResult> {
9
+ try {
10
+ logger.debug('ATTService: Initializing ATT permissions');
11
+ const result = await trackingPermissions.initialize();
12
+ logger.debug('ATTService: ATT permissions initialized', {
13
+ status: result.status,
14
+ canTrack: result.canTrack
15
+ });
16
+ return result;
17
+ } catch (error) {
18
+ logger.error('ATTService: Failed to initialize ATT permissions', { error });
19
+ throw error;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Request ATT permissions from user
25
+ */
26
+ async requestPermissions(): Promise<TrackingPermissionResult> {
27
+ try {
28
+ logger.debug('ATTService: Requesting ATT permissions');
29
+ const result = await trackingPermissions.requestPermissions();
30
+ logger.debug('ATTService: ATT permissions request completed', {
31
+ status: result.status,
32
+ canTrack: result.canTrack
33
+ });
34
+ return result;
35
+ } catch (error) {
36
+ logger.error('ATTService: Failed to request ATT permissions', { error });
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Get current permission status
43
+ */
44
+ getCurrentStatus() {
45
+ return trackingPermissions.currentStatus;
46
+ }
47
+
48
+ /**
49
+ * Check if tracking is supported on this platform
50
+ */
51
+ isTrackingSupported(): boolean {
52
+ return trackingPermissions.isTrackingSupported;
53
+ }
54
+
55
+ /**
56
+ * Check if user can be tracked
57
+ */
58
+ canTrack(): boolean {
59
+ return trackingPermissions.canTrack;
60
+ }
61
+
62
+ /**
63
+ * Get advertising ID if available
64
+ */
65
+ async getAdvertisingId(): Promise<string | null> {
66
+ try {
67
+ return await trackingPermissions.getAdvertisingId();
68
+ } catch (error) {
69
+ logger.error('ATTService: Failed to get advertising ID', { error });
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Reset ATT service state
76
+ */
77
+ reset(): void {
78
+ trackingPermissions.reset();
79
+ logger.debug('ATTService: Service reset');
80
+ }
81
+ }
82
+
83
+ // Export singleton instance
84
+ export const attService = new ATTService();
@@ -0,0 +1,208 @@
1
+ import * as TrackingTransparency from 'expo-tracking-transparency';
2
+ import { logger } from '../utils/logger';
3
+
4
+ export enum TrackingStatus {
5
+ NOT_DETERMINED = 'not-determined',
6
+ RESTRICTED = 'restricted',
7
+ DENIED = 'denied',
8
+ GRANTED = 'granted',
9
+ UNSUPPORTED = 'unsupported' // For non-iOS platforms
10
+ }
11
+
12
+ export interface TrackingPermissionResult {
13
+ status: TrackingStatus;
14
+ canTrack: boolean;
15
+ advertisingId?: string;
16
+ }
17
+
18
+ class TrackingPermissionsService {
19
+ private _currentStatus: TrackingStatus = TrackingStatus.NOT_DETERMINED;
20
+ private _isInitialized: boolean = false;
21
+
22
+ /**
23
+ * Get current tracking permission status
24
+ */
25
+ get currentStatus(): TrackingStatus {
26
+ return this._currentStatus;
27
+ }
28
+
29
+ /**
30
+ * Check if tracking is supported on this platform
31
+ */
32
+ get isTrackingSupported(): boolean {
33
+ return TrackingTransparency.isAvailable();
34
+ }
35
+
36
+ /**
37
+ * Check if user can be tracked
38
+ */
39
+ get canTrack(): boolean {
40
+ return this._currentStatus === TrackingStatus.GRANTED;
41
+ }
42
+
43
+ /**
44
+ * Initialize the service and get current permission status
45
+ */
46
+ async initialize(): Promise<TrackingPermissionResult> {
47
+ logger.debug('TrackingPermissions: Initializing...');
48
+
49
+ if (!this.isTrackingSupported) {
50
+ logger.info('TrackingPermissions: ATT not available on this platform/version');
51
+ this._currentStatus = TrackingStatus.UNSUPPORTED;
52
+ this._isInitialized = true;
53
+ return {
54
+ status: this._currentStatus,
55
+ canTrack: true // Allow tracking when ATT is not required
56
+ };
57
+ }
58
+
59
+ try {
60
+ // Get current permissions without requesting
61
+ const { status } = await TrackingTransparency.getTrackingPermissionsAsync();
62
+ this._currentStatus = this._mapExpoStatusToOurStatus(status);
63
+ this._isInitialized = true;
64
+
65
+ logger.debug('TrackingPermissions: Current status retrieved', {
66
+ status: this._currentStatus,
67
+ canTrack: this.canTrack
68
+ });
69
+
70
+ return await this._buildResult();
71
+ } catch (error) {
72
+ logger.error('TrackingPermissions: Failed to get current status', { error });
73
+ this._currentStatus = TrackingStatus.NOT_DETERMINED;
74
+ this._isInitialized = true;
75
+ return {
76
+ status: this._currentStatus,
77
+ canTrack: false
78
+ };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Request tracking permissions from the user
84
+ */
85
+ async requestPermissions(): Promise<TrackingPermissionResult> {
86
+ logger.debug('TrackingPermissions: Requesting permissions...');
87
+
88
+ if (!this.isTrackingSupported) {
89
+ logger.info('TrackingPermissions: ATT not available, returning unsupported');
90
+ return {
91
+ status: TrackingStatus.UNSUPPORTED,
92
+ canTrack: true
93
+ };
94
+ }
95
+
96
+ try {
97
+ // Check current status first
98
+ if (!this._isInitialized) {
99
+ await this.initialize();
100
+ }
101
+
102
+ // If already determined, don't request again
103
+ if (this._currentStatus === TrackingStatus.GRANTED ||
104
+ this._currentStatus === TrackingStatus.DENIED ||
105
+ this._currentStatus === TrackingStatus.RESTRICTED) {
106
+ logger.info('TrackingPermissions: Status already determined', {
107
+ status: this._currentStatus
108
+ });
109
+ return await this._buildResult();
110
+ }
111
+
112
+ // Request permissions
113
+ const { status } = await TrackingTransparency.requestTrackingPermissionsAsync();
114
+ this._currentStatus = this._mapExpoStatusToOurStatus(status);
115
+
116
+ logger.debug('TrackingPermissions: Permission request completed', {
117
+ status: this._currentStatus,
118
+ canTrack: this.canTrack
119
+ });
120
+
121
+ return await this._buildResult();
122
+ } catch (error) {
123
+ logger.error('TrackingPermissions: Failed to request permissions', { error });
124
+ this._currentStatus = TrackingStatus.DENIED;
125
+ return {
126
+ status: this._currentStatus,
127
+ canTrack: false
128
+ };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Get advertising ID if tracking is allowed
134
+ */
135
+ async getAdvertisingId(): Promise<string | null> {
136
+ if (!this.isTrackingSupported) {
137
+ logger.info('TrackingPermissions: ATT not available, cannot get advertising ID from this API');
138
+ return null; // Let Adjust SDK handle ADID when ATT is not available
139
+ }
140
+
141
+ if (!this.canTrack) {
142
+ logger.info('TrackingPermissions: Cannot get advertising ID - tracking not granted');
143
+ return null;
144
+ }
145
+
146
+ try {
147
+ const advertisingId = TrackingTransparency.getAdvertisingId();
148
+ logger.info('TrackingPermissions: Retrieved advertising ID', {
149
+ hasId: !!advertisingId
150
+ });
151
+ return advertisingId;
152
+ } catch (error) {
153
+ logger.error('TrackingPermissions: Failed to get advertising ID', { error });
154
+ return null;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Reset the service state (useful for testing)
160
+ */
161
+ reset(): void {
162
+ this._currentStatus = TrackingStatus.NOT_DETERMINED;
163
+ this._isInitialized = false;
164
+ logger.debug('TrackingPermissions: Service reset');
165
+ }
166
+
167
+ /**
168
+ * Map Expo's tracking status to our internal status
169
+ */
170
+ private _mapExpoStatusToOurStatus(expoStatus: string): TrackingStatus {
171
+ switch (expoStatus) {
172
+ case 'granted':
173
+ return TrackingStatus.GRANTED;
174
+ case 'denied':
175
+ return TrackingStatus.DENIED;
176
+ case 'restricted':
177
+ return TrackingStatus.RESTRICTED;
178
+ case 'not-determined':
179
+ case 'undetermined': // Handle both possible values
180
+ default:
181
+ return TrackingStatus.NOT_DETERMINED;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Build a complete result object
187
+ */
188
+ private async _buildResult(): Promise<TrackingPermissionResult> {
189
+ const result: TrackingPermissionResult = {
190
+ status: this._currentStatus,
191
+ canTrack: this.canTrack
192
+ };
193
+
194
+ // Try to get advertising ID if tracking is allowed
195
+ if (this.canTrack && this.isTrackingSupported) {
196
+ try {
197
+ result.advertisingId = (await this.getAdvertisingId()) || undefined;
198
+ } catch (error) {
199
+ logger.error('TrackingPermissions: Failed to get advertising ID for result', { error });
200
+ }
201
+ }
202
+
203
+ return result;
204
+ }
205
+ }
206
+
207
+ // Export singleton instance
208
+ export const trackingPermissions = new TrackingPermissionsService();
@@ -0,0 +1,162 @@
1
+ import { create } from 'zustand';
2
+ import { attService } from '../services/attService';
3
+ import { TrackingStatus, TrackingPermissionResult } from '../services/trackingPermissions';
4
+ import { logger } from '../utils/logger';
5
+
6
+ interface ATTState {
7
+ // State
8
+ permissionStatus: TrackingStatus;
9
+ isPermissionRequested: boolean;
10
+ isLoading: boolean;
11
+ error: string | null;
12
+
13
+ // Actions
14
+ initialize: () => Promise<void>;
15
+ requestPermissions: () => Promise<TrackingPermissionResult>;
16
+ setLoading: (loading: boolean) => void;
17
+ setError: (error: string | null) => void;
18
+ reset: () => void;
19
+
20
+ // Computed values
21
+ canTrack: boolean;
22
+ isTrackingSupported: boolean;
23
+ }
24
+
25
+ export const useATTStore = create<ATTState>((set, get) => ({
26
+ // Initial state
27
+ permissionStatus: TrackingStatus.NOT_DETERMINED,
28
+ isPermissionRequested: false,
29
+ isLoading: false,
30
+ error: null,
31
+
32
+ // Actions
33
+ initialize: async () => {
34
+ const state = get();
35
+ if (state.isLoading) {
36
+ logger.info('ATTStore: Already initializing, skipping');
37
+ return;
38
+ }
39
+
40
+ try {
41
+ set({ isLoading: true, error: null });
42
+ logger.info('ATTStore: Initializing ATT permissions...');
43
+
44
+ const result = await attService.initialize();
45
+
46
+ set({
47
+ permissionStatus: result.status,
48
+ isLoading: false,
49
+ error: null
50
+ });
51
+
52
+ logger.info('ATTStore: ATT permissions initialized', {
53
+ status: result.status,
54
+ canTrack: result.canTrack
55
+ });
56
+ } catch (error) {
57
+ const errorMessage = error instanceof Error ? error.message : 'Failed to initialize ATT permissions';
58
+ logger.error('ATTStore: Failed to initialize ATT permissions', { error });
59
+ set({
60
+ error: errorMessage,
61
+ isLoading: false,
62
+ permissionStatus: TrackingStatus.DENIED
63
+ });
64
+ }
65
+ },
66
+
67
+ requestPermissions: async () => {
68
+ const state = get();
69
+ if (state.isLoading) {
70
+ logger.info('ATTStore: Permission request already in progress');
71
+ return { status: state.permissionStatus, canTrack: state.canTrack };
72
+ }
73
+
74
+ try {
75
+ set({ isLoading: true, error: null });
76
+ logger.info('ATTStore: Requesting ATT permissions...');
77
+
78
+ const result = await attService.requestPermissions();
79
+
80
+ set({
81
+ permissionStatus: result.status,
82
+ isPermissionRequested: true,
83
+ isLoading: false,
84
+ error: null
85
+ });
86
+
87
+ logger.info('ATTStore: ATT permissions requested', {
88
+ status: result.status,
89
+ canTrack: result.canTrack
90
+ });
91
+
92
+ return result;
93
+ } catch (error) {
94
+ const errorMessage = error instanceof Error ? error.message : 'Failed to request ATT permissions';
95
+ logger.error('ATTStore: Failed to request ATT permissions', { error });
96
+
97
+ const result = {
98
+ status: TrackingStatus.DENIED,
99
+ canTrack: false
100
+ };
101
+
102
+ set({
103
+ error: errorMessage,
104
+ isLoading: false,
105
+ permissionStatus: TrackingStatus.DENIED,
106
+ isPermissionRequested: true
107
+ });
108
+
109
+ return result;
110
+ }
111
+ },
112
+
113
+ setLoading: (isLoading) => set({ isLoading }),
114
+
115
+ setError: (error) => set({ error }),
116
+
117
+ reset: () => {
118
+ set({
119
+ permissionStatus: TrackingStatus.NOT_DETERMINED,
120
+ isPermissionRequested: false,
121
+ isLoading: false,
122
+ error: null
123
+ });
124
+ attService.reset();
125
+ logger.info('ATTStore: Store reset');
126
+ },
127
+
128
+ // Computed values
129
+ get canTrack() {
130
+ const state = get();
131
+ return state.permissionStatus === TrackingStatus.GRANTED ||
132
+ state.permissionStatus === TrackingStatus.UNSUPPORTED;
133
+ },
134
+
135
+ get isTrackingSupported() {
136
+ return attService.isTrackingSupported();
137
+ },
138
+ }));
139
+
140
+ // Selectors for commonly used ATT state
141
+ export const useATT = () => {
142
+ const state = useATTStore();
143
+ return {
144
+ permissionStatus: state.permissionStatus,
145
+ isPermissionRequested: state.isPermissionRequested,
146
+ isLoading: state.isLoading,
147
+ error: state.error,
148
+ canTrack: state.canTrack,
149
+ isTrackingSupported: state.isTrackingSupported,
150
+ };
151
+ };
152
+
153
+ export const useATTActions = () => {
154
+ const state = useATTStore();
155
+ return {
156
+ initialize: state.initialize,
157
+ requestPermissions: state.requestPermissions,
158
+ setLoading: state.setLoading,
159
+ setError: state.setError,
160
+ reset: state.reset,
161
+ };
162
+ };
@@ -0,0 +1,174 @@
1
+ import { Platform } from 'react-native';
2
+ import Purchases, { CustomerInfo, PurchasesPackage, PurchasesOfferings } from 'react-native-purchases';
3
+ import Constants from 'expo-constants';
4
+ import { logger } from '../utils/logger';
5
+
6
+ class RevenueCatService {
7
+ private static readonly IOS_API_KEY = Constants.expoConfig?.extra?.revenueCat?.iosKey || '';
8
+ private static readonly ANDROID_API_KEY = Constants.expoConfig?.extra?.revenueCat?.androidKey || '';
9
+
10
+ private _isInitialized: boolean = false;
11
+
12
+ /**
13
+ * Check if RevenueCat is initialized
14
+ */
15
+ get isInitialized(): boolean {
16
+ return this._isInitialized;
17
+ }
18
+
19
+ /**
20
+ * Initialize RevenueCat SDK
21
+ */
22
+ initialize(): void {
23
+ if (this._isInitialized) {
24
+ logger.info('RevenueCatService: Already initialized, skipping');
25
+ return;
26
+ }
27
+
28
+ try {
29
+ logger.info('RevenueCatService: Initializing RevenueCat...');
30
+
31
+ // Set log level for debugging
32
+ Purchases.setLogLevel(Purchases.LOG_LEVEL.DEBUG);
33
+
34
+ // Configure with platform-specific API key
35
+ if (Platform.OS === 'ios') {
36
+ Purchases.configure({ apiKey: RevenueCatService.IOS_API_KEY });
37
+ } else if (Platform.OS === 'android') {
38
+ Purchases.configure({ apiKey: RevenueCatService.ANDROID_API_KEY });
39
+ }
40
+
41
+ this._isInitialized = true;
42
+ logger.info('RevenueCatService: RevenueCat initialized successfully');
43
+ } catch (error) {
44
+ logger.error('RevenueCatService: Failed to initialize RevenueCat', { error });
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Set Adjust ID for attribution
51
+ */
52
+ setAdjustId(adjustId: string): void {
53
+ try {
54
+ logger.info('RevenueCatService: Setting Adjust ID', { adjustId });
55
+ Purchases.setAdjustID(adjustId);
56
+ logger.info('RevenueCatService: Adjust ID set successfully');
57
+ } catch (error) {
58
+ logger.error('RevenueCatService: Failed to set Adjust ID', { error });
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get customer info
65
+ */
66
+ async getCustomerInfo(): Promise<CustomerInfo> {
67
+ try {
68
+ logger.info('RevenueCatService: Getting customer info');
69
+ const customerInfo = await Purchases.getCustomerInfo();
70
+ logger.info('RevenueCatService: Customer info retrieved successfully');
71
+ return customerInfo;
72
+ } catch (error) {
73
+ logger.error('RevenueCatService: Failed to get customer info', { error });
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Get available offerings
80
+ */
81
+ async getOfferings(): Promise<PurchasesOfferings> {
82
+ try {
83
+ logger.info('RevenueCatService: Getting offerings');
84
+ const offerings = await Purchases.getOfferings();
85
+ logger.info('RevenueCatService: Offerings retrieved successfully', {
86
+ offeringsCount: Object.keys(offerings.all).length,
87
+ hasCurrent: !!offerings.current,
88
+ });
89
+ return offerings;
90
+ } catch (error) {
91
+ logger.error('RevenueCatService: Failed to get offerings', { error });
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Purchase a package
98
+ */
99
+ async purchasePackage(pkg: PurchasesPackage): Promise<{
100
+ customerInfo: CustomerInfo;
101
+ userCancelled: boolean;
102
+ }> {
103
+ try {
104
+ logger.info('RevenueCatService: Purchasing package', { packageId: pkg.identifier });
105
+ const result = await Purchases.purchasePackage(pkg);
106
+ logger.info('RevenueCatService: Package purchased successfully');
107
+ return { ...result, userCancelled: false };
108
+ } catch (error: any) {
109
+ if (error.userCancelled) {
110
+ logger.info('RevenueCatService: Purchase cancelled by user');
111
+ return {
112
+ customerInfo: await this.getCustomerInfo(),
113
+ userCancelled: true
114
+ };
115
+ }
116
+ logger.error('RevenueCatService: Failed to purchase package', { error });
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Restore purchases
123
+ */
124
+ async restorePurchases(): Promise<CustomerInfo> {
125
+ try {
126
+ logger.info('RevenueCatService: Restoring purchases');
127
+ const customerInfo = await Purchases.restorePurchases();
128
+ logger.info('RevenueCatService: Purchases restored successfully');
129
+ return customerInfo;
130
+ } catch (error) {
131
+ logger.error('RevenueCatService: Failed to restore purchases', { error });
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Check if user has active entitlement
138
+ */
139
+ hasActiveEntitlement(customerInfo: CustomerInfo, entitlementId: string): boolean {
140
+ return customerInfo.entitlements.active[entitlementId] !== undefined;
141
+ }
142
+
143
+ /**
144
+ * Get entitlement info
145
+ */
146
+ getEntitlementInfo(customerInfo: CustomerInfo, entitlementId: string) {
147
+ return customerInfo.entitlements.active[entitlementId];
148
+ }
149
+
150
+ /**
151
+ * Add customer info update listener
152
+ */
153
+ addCustomerInfoUpdateListener(callback: (customerInfo: CustomerInfo) => void): void {
154
+ Purchases.addCustomerInfoUpdateListener(callback);
155
+ }
156
+
157
+ /**
158
+ * Remove customer info update listener
159
+ */
160
+ removeCustomerInfoUpdateListener(callback: (customerInfo: CustomerInfo) => void): void {
161
+ Purchases.removeCustomerInfoUpdateListener(callback);
162
+ }
163
+
164
+ /**
165
+ * Reset RevenueCat state (useful for testing)
166
+ */
167
+ reset(): void {
168
+ this._isInitialized = false;
169
+ logger.info('RevenueCatService: Service reset');
170
+ }
171
+ }
172
+
173
+ // Export singleton instance
174
+ export const revenueCatService = new RevenueCatService();