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.
- package/LICENSE +21 -0
- package/README.md +642 -0
- package/bin/cli.js +12 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/dependencies.d.ts +82 -0
- package/dist/config/dependencies.d.ts.map +1 -0
- package/dist/config/dependencies.js +82 -0
- package/dist/config/dependencies.js.map +1 -0
- package/dist/config/presets.d.ts +3 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/presets.js +174 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/generators/index.d.ts +40 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +130 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/onboarding.d.ts +8 -0
- package/dist/generators/onboarding.d.ts.map +1 -0
- package/dist/generators/onboarding.js +141 -0
- package/dist/generators/onboarding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/features.d.ts +14 -0
- package/dist/prompts/features.d.ts.map +1 -0
- package/dist/prompts/features.js +96 -0
- package/dist/prompts/features.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +93 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/onboarding.d.ts +6 -0
- package/dist/prompts/onboarding.d.ts.map +1 -0
- package/dist/prompts/onboarding.js +37 -0
- package/dist/prompts/onboarding.js.map +1 -0
- package/dist/prompts/orm.d.ts +3 -0
- package/dist/prompts/orm.d.ts.map +1 -0
- package/dist/prompts/orm.js +23 -0
- package/dist/prompts/orm.js.map +1 -0
- package/dist/prompts/packageManager.d.ts +2 -0
- package/dist/prompts/packageManager.d.ts.map +1 -0
- package/dist/prompts/packageManager.js +18 -0
- package/dist/prompts/packageManager.js.map +1 -0
- package/dist/prompts/platform.d.ts +3 -0
- package/dist/prompts/platform.d.ts.map +1 -0
- package/dist/prompts/platform.js +21 -0
- package/dist/prompts/platform.js.map +1 -0
- package/dist/prompts/preset.d.ts +4 -0
- package/dist/prompts/preset.d.ts.map +1 -0
- package/dist/prompts/preset.js +165 -0
- package/dist/prompts/preset.js.map +1 -0
- package/dist/prompts/project.d.ts +2 -0
- package/dist/prompts/project.d.ts.map +1 -0
- package/dist/prompts/project.js +27 -0
- package/dist/prompts/project.js.map +1 -0
- package/dist/prompts/sdks.d.ts +2 -0
- package/dist/prompts/sdks.d.ts.map +1 -0
- package/dist/prompts/sdks.js +46 -0
- package/dist/prompts/sdks.js.map +1 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cleanup.d.ts +5 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +38 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/copy.d.ts +10 -0
- package/dist/utils/copy.d.ts.map +1 -0
- package/dist/utils/copy.js +53 -0
- package/dist/utils/copy.js.map +1 -0
- package/dist/utils/errors.d.ts +33 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +136 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +33 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package.d.ts +16 -0
- package/dist/utils/package.d.ts.map +1 -0
- package/dist/utils/package.js +86 -0
- package/dist/utils/package.js.map +1 -0
- package/dist/utils/system-validation.d.ts +9 -0
- package/dist/utils/system-validation.d.ts.map +1 -0
- package/dist/utils/system-validation.js +31 -0
- package/dist/utils/system-validation.js.map +1 -0
- package/dist/utils/template.d.ts +20 -0
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +234 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +94 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +96 -0
- package/templates/base/backend/.dockerignore.ejs +62 -0
- package/templates/base/backend/.env.example.ejs +116 -0
- package/templates/base/backend/Dockerfile.ejs +142 -0
- package/templates/base/backend/controllers/event-queue/index.ts +20 -0
- package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
- package/templates/base/backend/controllers/rest-api/index.ts +48 -0
- package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
- package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
- package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
- package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
- package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
- package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
- package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
- package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
- package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
- package/templates/base/backend/domain/device-session/schema.ts +72 -0
- package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
- package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
- package/templates/base/backend/domain/session/schema.ts +29 -0
- package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
- package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
- package/templates/base/backend/domain/user/schema.ts +14 -0
- package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
- package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
- package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
- package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
- package/templates/base/backend/lib/constants.ts.ejs +29 -0
- package/templates/base/backend/package.json.ejs +50 -0
- package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
- package/templates/base/backend/prisma.config.prisma.ts +12 -0
- package/templates/base/backend/tsconfig.json +39 -0
- package/templates/base/backend/utils/db.drizzle.ts +41 -0
- package/templates/base/backend/utils/db.prisma.ts +51 -0
- package/templates/base/backend/utils/email.ts.ejs +35 -0
- package/templates/base/backend/utils/errors.ts +348 -0
- package/templates/base/backend/utils/redis.ts.ejs +279 -0
- package/templates/base/mobile/.env.example.ejs +35 -0
- package/templates/base/mobile/.gitignore.ejs +167 -0
- package/templates/base/mobile/app/+not-found.tsx +85 -0
- package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
- package/templates/base/mobile/app.json.ejs +88 -0
- package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
- package/templates/base/mobile/assets/images/favicon.png +0 -0
- package/templates/base/mobile/assets/images/icon.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
- package/templates/base/mobile/assets/images/paywall_image.png +0 -0
- package/templates/base/mobile/assets/images/splash.png +0 -0
- package/templates/base/mobile/eas.json.ejs +49 -0
- package/templates/base/mobile/metro.config.js +9 -0
- package/templates/base/mobile/package.json.ejs +53 -0
- package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
- package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
- package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
- package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
- package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
- package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
- package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
- package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
- package/templates/base/mobile/src/components/ui/index.ts +6 -0
- package/templates/base/mobile/src/constants/Theme.ts +163 -0
- package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
- package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
- package/templates/base/mobile/src/services/api.ts.ejs +71 -0
- package/templates/base/mobile/src/services/errorService.ts +179 -0
- package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
- package/templates/base/mobile/src/store/index.ts.ejs +18 -0
- package/templates/base/mobile/src/store/ui.store.ts +100 -0
- package/templates/base/mobile/src/utils/formatters.ts +105 -0
- package/templates/base/mobile/src/utils/logger.ts +73 -0
- package/templates/base/mobile/src/utils/responsive.ts +234 -0
- package/templates/base/mobile/tsconfig.json +32 -0
- package/templates/base/web/.env.example.ejs +26 -0
- package/templates/base/web/components.json +22 -0
- package/templates/base/web/eslint.config.mjs +18 -0
- package/templates/base/web/next.config.ts +7 -0
- package/templates/base/web/package.json.ejs +35 -0
- package/templates/base/web/postcss.config.mjs +7 -0
- package/templates/base/web/public/.gitkeep +0 -0
- package/templates/base/web/public/file.svg +1 -0
- package/templates/base/web/public/globe.svg +1 -0
- package/templates/base/web/public/next.svg +1 -0
- package/templates/base/web/public/vercel.svg +1 -0
- package/templates/base/web/public/window.svg +1 -0
- package/templates/base/web/src/app/favicon.ico +0 -0
- package/templates/base/web/src/app/globals.css +152 -0
- package/templates/base/web/src/app/layout.tsx.ejs +54 -0
- package/templates/base/web/src/app/page.tsx.ejs +92 -0
- package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
- package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
- package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
- package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
- package/templates/base/web/src/components/theme-toggle.tsx +34 -0
- package/templates/base/web/src/components/ui/button.tsx +62 -0
- package/templates/base/web/src/components/ui/card.tsx +92 -0
- package/templates/base/web/src/components/ui/input.tsx +21 -0
- package/templates/base/web/src/components/ui/label.tsx +24 -0
- package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/web/src/components/ui/spinner.tsx +20 -0
- package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
- package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
- package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
- package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
- package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
- package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
- package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
- package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
- package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
- package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
- package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
- package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
- package/templates/base/web/src/lib/utils.ts +6 -0
- package/templates/base/web/src/proxy.ts.ejs +66 -0
- package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
- package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
- package/templates/base/web/tsconfig.json +34 -0
- package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
- package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
- package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
- package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
- package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
- package/templates/features/mobile/auth/components/auth/index.ts +2 -0
- package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
- package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
- package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
- package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
- package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
- package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
- package/templates/features/web/.gitkeep +0 -0
- package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
- package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
- package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
- package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
- package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
- package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
- package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
- package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
- package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
- package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
- package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
- package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
- package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
- package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
- package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
- package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
- package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
- package/templates/integrations/mobile/att/services/attService.ts +84 -0
- package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
- package/templates/integrations/mobile/att/store/att.store.ts +162 -0
- package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
- package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
- package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
- package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
- package/templates/integrations/web/.gitkeep +0 -0
- package/templates/shared/.env.example.ejs +21 -0
- package/templates/shared/.gitignore.ejs +145 -0
- package/templates/shared/README.md.ejs +134 -0
- package/templates/shared/docker-compose.prod.yml.ejs +120 -0
- package/templates/shared/docker-compose.yml.ejs +129 -0
- package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
- package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
- 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();
|