@umituz/react-native-subscription 2.27.66 → 2.27.68

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 (125) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/CreditLimitCalculator.ts +17 -0
  3. package/src/domains/credits/application/CreditsInitializer.ts +85 -0
  4. package/src/domains/credits/application/DeductCreditsCommand.ts +52 -0
  5. package/src/domains/credits/application/PurchaseMetadataGenerator.ts +59 -0
  6. package/src/domains/credits/application/credit-strategies/CreditAllocationContext.ts +35 -0
  7. package/src/domains/credits/application/credit-strategies/ICreditStrategy.ts +18 -0
  8. package/src/domains/credits/application/credit-strategies/StandardPurchaseCreditStrategy.ts +16 -0
  9. package/src/domains/credits/application/credit-strategies/SyncCreditStrategy.ts +15 -0
  10. package/src/domains/credits/application/credit-strategies/TrialCreditStrategy.ts +18 -0
  11. package/src/{infrastructure/mappers → domains/credits/core}/CreditsMapper.ts +4 -4
  12. package/src/domains/credits/infrastructure/CreditsRepository.ts +102 -0
  13. package/src/{presentation/hooks → domains/credits/presentation}/useCredits.ts +21 -4
  14. package/src/domains/subscription/application/SubscriptionAuthListener.ts +26 -0
  15. package/src/domains/subscription/application/SubscriptionInitializer.ts +77 -0
  16. package/src/{infrastructure/services → domains/subscription/application}/SubscriptionInitializerTypes.ts +21 -1
  17. package/src/domains/subscription/application/SubscriptionSyncService.ts +71 -0
  18. package/src/domains/subscription/application/SubscriptionSyncUtils.ts +16 -0
  19. package/src/{revenuecat/domain/value-objects → domains/subscription/core}/RevenueCatConfig.ts +1 -1
  20. package/src/{domain/types → domains/subscription/core}/RevenueCatData.ts +1 -1
  21. package/src/{domain/entities → domains/subscription/core}/SubscriptionStatus.ts +13 -21
  22. package/src/domains/subscription/core/SubscriptionStatusHandlers.ts +51 -0
  23. package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +67 -0
  24. package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +27 -0
  25. package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRevenueCat.ts +1 -1
  26. package/src/domains/subscription/infrastructure/managers/SubscriptionInternalState.ts +12 -0
  27. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +110 -0
  28. package/src/{revenuecat → domains/subscription}/infrastructure/services/PurchaseHandler.ts +1 -1
  29. package/src/{revenuecat → domains/subscription}/infrastructure/services/RestoreHandler.ts +1 -1
  30. package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatInitializer.ts +1 -1
  31. package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatService.ts +2 -2
  32. package/src/{presentation/hooks → domains/subscription/presentation}/usePremium.ts +7 -4
  33. package/src/domains/trial/application/TrialEligibilityService.ts +25 -0
  34. package/src/domains/trial/application/TrialService.ts +69 -0
  35. package/src/{infrastructure/services → domains/trial/core}/TrialTypes.ts +1 -1
  36. package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +30 -0
  37. package/src/index.ts +28 -59
  38. package/src/init/createSubscriptionInitModule.ts +1 -1
  39. package/src/presentation/components/details/PremiumStatusBadge.tsx +1 -3
  40. package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +1 -1
  41. package/src/presentation/hooks/index.ts +11 -11
  42. package/src/shared/application/ports/IRevenueCatService.ts +32 -0
  43. package/src/shared/infrastructure/SubscriptionEventBus.ts +51 -0
  44. package/src/application/README.md +0 -50
  45. package/src/domain/README.md +0 -54
  46. package/src/domain/entities/README.md +0 -50
  47. package/src/domain/entities/SubscriptionStatus.test.ts +0 -149
  48. package/src/domain/errors/README.md +0 -53
  49. package/src/domain/value-objects/README.md +0 -50
  50. package/src/infrastructure/README.md +0 -55
  51. package/src/infrastructure/mappers/README.md +0 -21
  52. package/src/infrastructure/models/README.md +0 -26
  53. package/src/infrastructure/repositories/CreditsRepository.ts +0 -132
  54. package/src/infrastructure/repositories/README.md +0 -99
  55. package/src/infrastructure/services/CreditsInitializer.ts +0 -170
  56. package/src/infrastructure/services/README.md +0 -99
  57. package/src/infrastructure/services/SubscriptionInitializer.ts +0 -176
  58. package/src/infrastructure/services/SubscriptionService.ts +0 -133
  59. package/src/infrastructure/services/TrialService.ts +0 -197
  60. package/src/infrastructure/services/app-service-helpers.ts +0 -111
  61. package/src/revenuecat/README.md +0 -104
  62. package/src/revenuecat/application/README.md +0 -43
  63. package/src/revenuecat/application/ports/IRevenueCatService.ts +0 -76
  64. package/src/revenuecat/application/ports/README.md +0 -41
  65. package/src/revenuecat/domain/README.md +0 -48
  66. package/src/revenuecat/domain/constants/README.md +0 -41
  67. package/src/revenuecat/domain/entities/README.md +0 -42
  68. package/src/revenuecat/domain/errors/README.md +0 -53
  69. package/src/revenuecat/domain/types/README.md +0 -41
  70. package/src/revenuecat/domain/value-objects/README.md +0 -41
  71. package/src/revenuecat/index.ts +0 -13
  72. package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +0 -161
  73. package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +0 -165
  74. package/src/revenuecat/presentation/README.md +0 -42
  75. /package/src/{domain/entities → domains/credits/core}/Credits.ts +0 -0
  76. /package/src/{infrastructure/models → domains/credits/core}/UserCreditsDocument.ts +0 -0
  77. /package/src/{infrastructure/repositories → domains/credits/infrastructure}/CreditsRepositoryProvider.ts +0 -0
  78. /package/src/{presentation/hooks → domains/credits/presentation}/useDeductCredit.ts +0 -0
  79. /package/src/{revenuecat/domain/constants → domains/subscription/core}/RevenueCatConstants.ts +0 -0
  80. /package/src/{revenuecat/domain/errors → domains/subscription/core}/RevenueCatError.ts +0 -0
  81. /package/src/{revenuecat/domain/types → domains/subscription/core}/RevenueCatTypes.ts +0 -0
  82. /package/src/{domain/entities → domains/subscription/core}/SubscriptionConstants.ts +0 -0
  83. /package/src/{revenuecat → domains/subscription}/infrastructure/README.md +0 -0
  84. /package/src/{revenuecat → domains/subscription}/infrastructure/config/README.md +0 -0
  85. /package/src/{revenuecat → domains/subscription}/infrastructure/handlers/README.md +0 -0
  86. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/README.md +0 -0
  87. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/subscriptionQueryKeys.ts +0 -0
  88. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useCustomerInfo.ts +0 -0
  89. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useInitializeSubscription.ts +0 -0
  90. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/usePaywallFlow.ts +0 -0
  91. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/usePurchasePackage.ts +0 -0
  92. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRestorePurchase.ts +0 -0
  93. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRevenueCatTrialEligibility.ts +0 -0
  94. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useSubscriptionPackages.ts +0 -0
  95. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useSubscriptionQueries.ts +0 -0
  96. /package/src/{revenuecat → domains/subscription}/infrastructure/managers/README.md +0 -0
  97. /package/src/{revenuecat → domains/subscription}/infrastructure/services/CustomerInfoListenerManager.ts +0 -0
  98. /package/src/{revenuecat → domains/subscription}/infrastructure/services/OfferingsFetcher.ts +0 -0
  99. /package/src/{revenuecat → domains/subscription}/infrastructure/services/README.md +0 -0
  100. /package/src/{revenuecat → domains/subscription}/infrastructure/services/ServiceStateManager.ts +0 -0
  101. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/ApiKeyResolver.ts +0 -0
  102. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/InitializationCache.ts +0 -0
  103. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/PremiumStatusSyncer.ts +0 -0
  104. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/README.md +0 -0
  105. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/RenewalDetector.ts +0 -0
  106. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/UserIdProvider.ts +0 -0
  107. /package/src/{presentation/hooks → domains/subscription/presentation}/useAuthAwarePurchase.ts +0 -0
  108. /package/src/{presentation/hooks → domains/subscription/presentation}/useAuthSubscriptionSync.ts +0 -0
  109. /package/src/{presentation/hooks → domains/subscription/presentation}/useFeatureGate.ts +0 -0
  110. /package/src/{presentation/hooks → domains/subscription/presentation}/usePaywallVisibility.ts +0 -0
  111. /package/src/{presentation/hooks → domains/subscription/presentation}/usePremiumGate.ts +0 -0
  112. /package/src/{presentation/hooks → domains/subscription/presentation}/useSavedPurchaseAutoExecution.ts +0 -0
  113. /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionSettingsConfig.ts +0 -0
  114. /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionSettingsConfig.utils.ts +0 -0
  115. /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionStatus.ts +0 -0
  116. /package/src/{infrastructure/services → shared/application}/ActivationHandler.ts +0 -0
  117. /package/src/{infrastructure/services → shared/application}/FeedbackService.ts +0 -0
  118. /package/src/{application → shared/application}/ports/ISubscriptionRepository.ts +0 -0
  119. /package/src/{application → shared/application}/ports/ISubscriptionService.ts +0 -0
  120. /package/src/{application → shared/application}/ports/README.md +0 -0
  121. /package/src/{domain/errors → shared/utils}/InsufficientCreditsError.ts +0 -0
  122. /package/src/{infrastructure → shared}/utils/Logger.ts +0 -0
  123. /package/src/{domain/value-objects → shared/utils}/Result.ts +0 -0
  124. /package/src/{domain/value-objects → shared/utils}/SubscriptionConfig.ts +0 -0
  125. /package/src/{domain/errors → shared/utils}/SubscriptionError.ts +0 -0
@@ -1,176 +0,0 @@
1
- /**
2
- * Subscription Initializer
3
- *
4
- * Uses RevenueCat best practices:
5
- * - Non-blocking initialization (fire and forget)
6
- * - Relies on CustomerInfoUpdateListener for state updates
7
- * - No manual timeouts - uses auth state listener with cleanup
8
- */
9
-
10
- import { Platform } from "react-native";
11
- import type { CustomerInfo } from "react-native-purchases";
12
- import { configureCreditsRepository, getCreditsRepository } from "../repositories/CreditsRepositoryProvider";
13
- import { SubscriptionManager } from "../../revenuecat/infrastructure/managers/SubscriptionManager";
14
- import { configureAuthProvider } from "../../presentation/hooks/useAuthAwarePurchase";
15
- import type { RevenueCatData } from "../../domain/types/RevenueCatData";
16
- import { type PeriodType, type PurchaseSource } from "../../domain/entities/SubscriptionConstants";
17
- import type { SubscriptionInitConfig, FirebaseAuthLike } from "./SubscriptionInitializerTypes";
18
-
19
- export type { FirebaseAuthLike, CreditPackageConfig, SubscriptionInitConfig } from "./SubscriptionInitializerTypes";
20
-
21
- /**
22
- * Gets the current user ID from Firebase auth.
23
- * Uses listener pattern for reliability instead of timeout.
24
- * Falls back immediately if no auth is available.
25
- */
26
- const getCurrentUserId = (getAuth: () => FirebaseAuthLike | null): string | undefined => {
27
- const auth = getAuth();
28
- if (!auth) return undefined;
29
- return auth.currentUser?.uid;
30
- };
31
-
32
- /**
33
- * Sets up auth state listener that will re-initialize subscription
34
- * when user auth state changes (login/logout).
35
- */
36
- const setupAuthStateListener = (
37
- getAuth: () => FirebaseAuthLike | null,
38
- onUserChange: (userId: string | undefined) => void
39
- ): (() => void) | null => {
40
- const auth = getAuth();
41
- if (!auth) return null;
42
-
43
- return auth.onAuthStateChanged((user) => {
44
- onUserChange(user?.uid);
45
- });
46
- };
47
-
48
- /** Extract RevenueCat data from CustomerInfo (Single Source of Truth) */
49
- const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId: string): RevenueCatData => {
50
- const entitlement = customerInfo.entitlements.active[entitlementId]
51
- ?? customerInfo.entitlements.all[entitlementId];
52
- return {
53
- expirationDate: entitlement?.expirationDate ?? customerInfo.latestExpirationDate ?? null,
54
- willRenew: entitlement?.willRenew ?? false,
55
- originalTransactionId: entitlement?.originalPurchaseDate ?? undefined,
56
- periodType: entitlement?.periodType as PeriodType | undefined,
57
- };
58
- };
59
-
60
- export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
61
- const {
62
- apiKey, apiKeyIos, apiKeyAndroid, entitlementId, credits,
63
- getAnonymousUserId, getFirebaseAuth, showAuthModal,
64
- onCreditsUpdated, creditPackages,
65
- } = config;
66
-
67
- const key = Platform.OS === 'ios' ? (apiKeyIos || apiKey || '') : (apiKeyAndroid || apiKey || '');
68
- if (!key) throw new Error('API key required');
69
-
70
- configureCreditsRepository({ ...credits, creditPackageAmounts: creditPackages?.amounts });
71
-
72
- const onPurchase = async (userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource) => {
73
- if (__DEV__) console.log('[SubscriptionInitializer] onPurchase:', { userId, productId, source });
74
- try {
75
- const revenueCatData = extractRevenueCatData(customerInfo, entitlementId);
76
- await getCreditsRepository().initializeCredits(userId, `purchase_${productId}_${Date.now()}`, productId, source, revenueCatData);
77
- onCreditsUpdated?.(userId);
78
- } catch (error) {
79
- if (__DEV__) console.error('[SubscriptionInitializer] Credits init failed:', error);
80
- }
81
- };
82
-
83
- const onRenewal = async (userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) => {
84
- if (__DEV__) console.log('[SubscriptionInitializer] onRenewal:', { userId, productId });
85
- try {
86
- const revenueCatData = extractRevenueCatData(customerInfo, entitlementId);
87
- revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
88
- await getCreditsRepository().initializeCredits(userId, `renewal_${productId}_${Date.now()}`, productId, "renewal", revenueCatData);
89
- onCreditsUpdated?.(userId);
90
- } catch (error) {
91
- if (__DEV__) console.error('[SubscriptionInitializer] Renewal credits init failed:', error);
92
- }
93
- };
94
-
95
- const onPremiumStatusChanged = async (
96
- userId: string, isPremium: boolean, productId?: string,
97
- expiresAt?: string, willRenew?: boolean, periodType?: PeriodType
98
- ) => {
99
- if (__DEV__) console.log('[SubscriptionInitializer] onPremiumStatusChanged:', { userId, isPremium, productId, willRenew, periodType });
100
- try {
101
- // If premium became false, check if actually expired or just canceled
102
- if (!isPremium && productId) {
103
- const isActuallyExpired = !expiresAt || new Date(expiresAt) < new Date();
104
- if (isActuallyExpired) {
105
- await getCreditsRepository().syncExpiredStatus(userId);
106
- if (__DEV__) console.log('[SubscriptionInitializer] Subscription expired, synced status');
107
- } else {
108
- if (__DEV__) console.log('[SubscriptionInitializer] Canceled but not expired, preserving until:', expiresAt);
109
- const revenueCatData: RevenueCatData = { expirationDate: expiresAt, willRenew: false, isPremium: true, periodType };
110
- await getCreditsRepository().initializeCredits(userId, `status_sync_canceled_${Date.now()}`, productId, "settings", revenueCatData);
111
- }
112
- onCreditsUpdated?.(userId);
113
- return;
114
- }
115
-
116
- // Premium user - initialize credits with subscription data
117
- const revenueCatData: RevenueCatData = { expirationDate: expiresAt ?? null, willRenew: willRenew ?? false, isPremium, periodType };
118
- await getCreditsRepository().initializeCredits(userId, `status_sync_${Date.now()}`, productId, "settings", revenueCatData);
119
- if (__DEV__) console.log('[SubscriptionInitializer] Premium status synced to Firestore');
120
- onCreditsUpdated?.(userId);
121
- } catch (error) {
122
- if (__DEV__) console.error('[SubscriptionInitializer] Premium status sync failed:', error);
123
- }
124
- };
125
-
126
- SubscriptionManager.configure({
127
- config: {
128
- apiKey: key,
129
- entitlementIdentifier: entitlementId,
130
- consumableProductIdentifiers: [creditPackages?.identifierPattern || 'credit'],
131
- onPurchaseCompleted: onPurchase,
132
- onRenewalDetected: onRenewal,
133
- onPremiumStatusChanged,
134
- onCreditsUpdated,
135
- },
136
- apiKey: key,
137
- getAnonymousUserId,
138
- });
139
-
140
- // Configure auth provider immediately (sync)
141
- configureAuthProvider({
142
- isAuthenticated: () => {
143
- const u = getFirebaseAuth()?.currentUser;
144
- return !!(u && !u.isAnonymous);
145
- },
146
- showAuthModal,
147
- });
148
-
149
- // Get initial user ID (sync - no waiting)
150
- const initialUserId = getCurrentUserId(getFirebaseAuth);
151
-
152
- /**
153
- * Non-blocking initialization (fire and forget)
154
- * RevenueCat best practice: Don't block on initialization.
155
- * The CustomerInfoUpdateListener will handle state updates reactively.
156
- */
157
- const initializeInBackground = async (userId?: string) => {
158
- try {
159
- await SubscriptionManager.initialize(userId);
160
- if (__DEV__) console.log('[SubscriptionInitializer] Background init complete');
161
- } catch (error) {
162
- // Non-critical - listener will handle updates
163
- if (__DEV__) console.log('[SubscriptionInitializer] Background init error (non-critical):', error);
164
- }
165
- };
166
-
167
- // Start initialization in background (non-blocking)
168
- initializeInBackground(initialUserId);
169
-
170
- // Set up auth state listener for reactive updates
171
- // When user logs in/out, re-initialize with new user ID
172
- setupAuthStateListener(getFirebaseAuth, (newUserId) => {
173
- if (__DEV__) console.log('[SubscriptionInitializer] Auth state changed:', newUserId ? 'logged in' : 'logged out');
174
- initializeInBackground(newUserId);
175
- });
176
- };
@@ -1,133 +0,0 @@
1
- /**
2
- * Subscription Service Implementation
3
- * Database-first subscription management
4
- */
5
-
6
- import { timezoneService } from "@umituz/react-native-design-system";
7
- import type { ISubscriptionService } from "../../application/ports/ISubscriptionService";
8
- import type { ISubscriptionRepository } from "../../application/ports/ISubscriptionRepository";
9
- import type { SubscriptionStatus } from "../../domain/entities/SubscriptionStatus";
10
- import { createDefaultSubscriptionStatus } from "../../domain/entities/SubscriptionStatus";
11
- import {
12
- SubscriptionRepositoryError,
13
- SubscriptionValidationError,
14
- } from "../../domain/errors/SubscriptionError";
15
- import type { SubscriptionConfig } from "../../domain/value-objects/SubscriptionConfig";
16
- import {
17
- activateSubscription,
18
- deactivateSubscription,
19
- safeHandleError,
20
- type ActivationHandlerConfig,
21
- } from "./ActivationHandler";
22
-
23
- export class SubscriptionService implements ISubscriptionService {
24
- private repository: ISubscriptionRepository;
25
- private handlerConfig: ActivationHandlerConfig;
26
-
27
- constructor(config: SubscriptionConfig) {
28
- if (!config.repository) {
29
- throw new SubscriptionValidationError("Repository is required");
30
- }
31
-
32
- this.repository = config.repository;
33
- this.handlerConfig = {
34
- repository: config.repository,
35
- onStatusChanged: config.onStatusChanged,
36
- onError: config.onError,
37
- };
38
- }
39
-
40
- async getSubscriptionStatus(userId: string): Promise<SubscriptionStatus> {
41
- try {
42
- const status = await this.repository.getSubscriptionStatus(userId);
43
- if (!status) {
44
- return createDefaultSubscriptionStatus();
45
- }
46
-
47
- const isValid = this.repository.isSubscriptionValid(status);
48
- if (!isValid && status.isPremium) {
49
- return await this.deactivateSubscription(userId);
50
- }
51
-
52
- return status;
53
- } catch (error) {
54
- await this.handleError(error, "getSubscriptionStatus");
55
- return createDefaultSubscriptionStatus();
56
- }
57
- }
58
-
59
- async isPremium(userId: string): Promise<boolean> {
60
- const status = await this.getSubscriptionStatus(userId);
61
- return this.repository.isSubscriptionValid(status);
62
- }
63
-
64
- async activateSubscription(
65
- userId: string,
66
- productId: string,
67
- expiresAt: string | null
68
- ): Promise<SubscriptionStatus> {
69
- return activateSubscription(
70
- this.handlerConfig,
71
- userId,
72
- productId,
73
- expiresAt
74
- );
75
- }
76
-
77
- async deactivateSubscription(userId: string): Promise<SubscriptionStatus> {
78
- return deactivateSubscription(this.handlerConfig, userId);
79
- }
80
-
81
- async updateSubscriptionStatus(
82
- userId: string,
83
- updates: Partial<SubscriptionStatus>
84
- ): Promise<SubscriptionStatus> {
85
- try {
86
- const updatesWithSync = {
87
- ...updates,
88
- syncedAt: timezoneService.getCurrentISOString(),
89
- };
90
-
91
- const updatedStatus = await this.repository.updateSubscriptionStatus(
92
- userId,
93
- updatesWithSync
94
- );
95
-
96
- if (this.handlerConfig.onStatusChanged) {
97
- try {
98
- await this.handlerConfig.onStatusChanged(userId, updatedStatus);
99
- } catch (error) {
100
- await this.handleError(error, "updateSubscriptionStatus.callback");
101
- }
102
- }
103
-
104
- return updatedStatus;
105
- } catch (error) {
106
- await this.handleError(error, "updateSubscriptionStatus");
107
- throw new SubscriptionRepositoryError("Failed to update subscription");
108
- }
109
- }
110
-
111
- private async handleError(error: unknown, context: string): Promise<void> {
112
- await safeHandleError(this.handlerConfig.onError, error, `SubscriptionService.${context}`);
113
- }
114
- }
115
-
116
- let subscriptionServiceInstance: SubscriptionService | null = null;
117
-
118
- export function initializeSubscriptionService(
119
- config: SubscriptionConfig
120
- ): SubscriptionService {
121
- if (!subscriptionServiceInstance) {
122
- subscriptionServiceInstance = new SubscriptionService(config);
123
- }
124
- return subscriptionServiceInstance;
125
- }
126
-
127
- export function getSubscriptionService(): SubscriptionService | null {
128
- return subscriptionServiceInstance;
129
- }
130
-
131
- export function resetSubscriptionService(): void {
132
- subscriptionServiceInstance = null;
133
- }
@@ -1,197 +0,0 @@
1
- /**
2
- * Trial Service - Device-based trial tracking to prevent abuse
3
- */
4
-
5
- import { doc, getDoc, setDoc, serverTimestamp, arrayUnion } from "firebase/firestore";
6
- import { getFirestore } from "@umituz/react-native-firebase";
7
- import { PersistentDeviceIdService } from "@umituz/react-native-design-system";
8
- import type { TrialEligibilityResult } from "./TrialTypes";
9
-
10
- export { TRIAL_CONFIG, type DeviceTrialRecord, type TrialEligibilityResult } from "./TrialTypes";
11
-
12
- const DEVICE_TRIALS_COLLECTION = "device_trials";
13
-
14
- /** Get persistent device ID */
15
- export async function getDeviceId(): Promise<string> {
16
- return PersistentDeviceIdService.getDeviceId();
17
- }
18
-
19
- /** Check if device is eligible for trial */
20
- export async function checkTrialEligibility(deviceId?: string): Promise<TrialEligibilityResult> {
21
- try {
22
- const effectiveDeviceId = deviceId || await getDeviceId();
23
- const db = getFirestore();
24
-
25
- if (!db) {
26
- if (__DEV__) {
27
- console.log("[TrialService] No Firestore instance");
28
- }
29
- return { eligible: true, deviceId: effectiveDeviceId };
30
- }
31
-
32
- const trialRef = doc(db, DEVICE_TRIALS_COLLECTION, effectiveDeviceId);
33
- const trialDoc = await getDoc(trialRef);
34
-
35
- if (!trialDoc.exists()) {
36
- if (__DEV__) {
37
- console.log("[TrialService] No trial record found, eligible");
38
- }
39
- return { eligible: true, deviceId: effectiveDeviceId };
40
- }
41
-
42
- const data = trialDoc.data();
43
- const hasUsedTrial = data?.hasUsedTrial === true;
44
- const trialInProgress = data?.trialInProgress === true;
45
-
46
- if (__DEV__) {
47
- console.log("[TrialService] Trial record found:", {
48
- deviceId: effectiveDeviceId.slice(0, 8),
49
- hasUsedTrial,
50
- trialInProgress,
51
- });
52
- }
53
-
54
- // Not eligible if trial was already used (converted or ended)
55
- // OR if trial is currently in progress
56
- if (hasUsedTrial || trialInProgress) {
57
- return {
58
- eligible: false,
59
- reason: "already_used",
60
- deviceId: effectiveDeviceId,
61
- };
62
- }
63
-
64
- return { eligible: true, deviceId: effectiveDeviceId };
65
- } catch (error) {
66
- if (__DEV__) {
67
- console.error("[TrialService] Eligibility check error:", error);
68
- }
69
- return { eligible: false, reason: "error" };
70
- }
71
- }
72
-
73
- /** Record trial start for a device */
74
- export async function recordTrialStart(userId: string, deviceId?: string): Promise<boolean> {
75
- try {
76
- const effectiveDeviceId = deviceId || await getDeviceId();
77
- const db = getFirestore();
78
-
79
- if (!db) {
80
- if (__DEV__) {
81
- console.log("[TrialService] No Firestore instance");
82
- }
83
- return false;
84
- }
85
-
86
- const trialRef = doc(db, DEVICE_TRIALS_COLLECTION, effectiveDeviceId);
87
-
88
- await setDoc(
89
- trialRef,
90
- {
91
- deviceId: effectiveDeviceId,
92
- trialInProgress: true,
93
- trialStartedAt: serverTimestamp(),
94
- lastUserId: userId,
95
- userIds: arrayUnion(userId),
96
- updatedAt: serverTimestamp(),
97
- },
98
- { merge: true }
99
- );
100
-
101
- // Also set createdAt if it's a new record
102
- const existingDoc = await getDoc(trialRef);
103
- if (!existingDoc.data()?.createdAt) {
104
- await setDoc(
105
- trialRef,
106
- { createdAt: serverTimestamp() },
107
- { merge: true }
108
- );
109
- }
110
-
111
- if (__DEV__) {
112
- console.log("[TrialService] Trial recorded:", {
113
- deviceId: effectiveDeviceId.slice(0, 8),
114
- userId: userId.slice(0, 8),
115
- });
116
- }
117
-
118
- return true;
119
- } catch (error) {
120
- if (__DEV__) {
121
- console.error("[TrialService] Record trial error:", error);
122
- }
123
- return false;
124
- }
125
- }
126
-
127
- /**
128
- * Record trial end (cancelled or expired)
129
- */
130
- export async function recordTrialEnd(deviceId?: string): Promise<boolean> {
131
- try {
132
- const effectiveDeviceId = deviceId || await getDeviceId();
133
- const db = getFirestore();
134
-
135
- if (!db) return false;
136
-
137
- const trialRef = doc(db, DEVICE_TRIALS_COLLECTION, effectiveDeviceId);
138
-
139
- await setDoc(
140
- trialRef,
141
- {
142
- hasUsedTrial: true,
143
- trialInProgress: false,
144
- trialEndedAt: serverTimestamp(),
145
- updatedAt: serverTimestamp(),
146
- },
147
- { merge: true }
148
- );
149
-
150
- if (__DEV__) {
151
- console.log("[TrialService] Trial end recorded - trial now consumed");
152
- }
153
-
154
- return true;
155
- } catch (error) {
156
- if (__DEV__) {
157
- console.error("[TrialService] Record trial end error:", error);
158
- }
159
- return false;
160
- }
161
- }
162
-
163
- /**
164
- * Record trial conversion to paid subscription
165
- */
166
- export async function recordTrialConversion(deviceId?: string): Promise<boolean> {
167
- try {
168
- const effectiveDeviceId = deviceId || await getDeviceId();
169
- const db = getFirestore();
170
-
171
- if (!db) return false;
172
-
173
- const trialRef = doc(db, DEVICE_TRIALS_COLLECTION, effectiveDeviceId);
174
-
175
- await setDoc(
176
- trialRef,
177
- {
178
- hasUsedTrial: true,
179
- trialInProgress: false,
180
- trialConvertedAt: serverTimestamp(),
181
- updatedAt: serverTimestamp(),
182
- },
183
- { merge: true }
184
- );
185
-
186
- if (__DEV__) {
187
- console.log("[TrialService] Trial conversion recorded - user converted to paid");
188
- }
189
-
190
- return true;
191
- } catch (error) {
192
- if (__DEV__) {
193
- console.error("[TrialService] Record conversion error:", error);
194
- }
195
- return false;
196
- }
197
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * App Service Helpers
3
- * Creates ready-to-use service implementations for configureAppServices
4
- */
5
-
6
- import { getCreditsRepository } from "../repositories/CreditsRepositoryProvider";
7
- import { getRevenueCatService } from "../../revenuecat/infrastructure/services/RevenueCatService";
8
- import { getPremiumEntitlement } from "../../revenuecat/domain/types/RevenueCatTypes";
9
- import { creditsQueryKeys } from "../../presentation/hooks/useCredits";
10
- import { paywallControl } from "../../presentation/hooks/usePaywallVisibility";
11
- import {
12
- getGlobalQueryClient,
13
- hasGlobalQueryClient,
14
- } from "@umituz/react-native-design-system";
15
- import {
16
- useAuthStore,
17
- selectUserId,
18
- } from "@umituz/react-native-auth";
19
-
20
- export interface CreditServiceConfig {
21
- entitlementId: string;
22
- }
23
-
24
- export interface ICreditService {
25
- checkCredits: (cost: number) => Promise<boolean>;
26
- deductCredits: (cost: number) => Promise<void>;
27
- refundCredits: (amount: number, error?: unknown) => Promise<void>;
28
- calculateCost: (capability: string, metadata?: Record<string, unknown>) => number;
29
- }
30
-
31
- export interface IPaywallService {
32
- showPaywall: (requiredCredits: number) => void;
33
- }
34
-
35
- const checkPremiumStatus = async (entitlementId: string): Promise<boolean> => {
36
- try {
37
- const rcService = getRevenueCatService();
38
- if (!rcService) return false;
39
-
40
- const customerInfo = await rcService.getCustomerInfo();
41
- if (!customerInfo) return false;
42
-
43
- return !!getPremiumEntitlement(customerInfo, entitlementId);
44
- } catch {
45
- return false;
46
- }
47
- };
48
-
49
- /**
50
- * Creates a credit service implementation
51
- */
52
- export function createCreditService(config: CreditServiceConfig): ICreditService {
53
- const { entitlementId } = config;
54
-
55
- return {
56
- checkCredits: async (cost: number): Promise<boolean> => {
57
- const userId = selectUserId(useAuthStore.getState());
58
- if (!userId) return false;
59
-
60
- // Premium users bypass credit check
61
- if (await checkPremiumStatus(entitlementId)) return true;
62
-
63
- try {
64
- const repository = getCreditsRepository();
65
- const result = await repository.getCredits(userId);
66
- if (!result.success || !result.data) return false;
67
- return (result.data.credits ?? 0) >= cost;
68
- } catch {
69
- return false;
70
- }
71
- },
72
-
73
- deductCredits: async (cost: number): Promise<void> => {
74
- const userId = selectUserId(useAuthStore.getState());
75
- if (!userId) return;
76
-
77
- // Premium users don't consume credits
78
- if (await checkPremiumStatus(entitlementId)) return;
79
-
80
- try {
81
- const repository = getCreditsRepository();
82
- await repository.deductCredit(userId, cost);
83
-
84
- if (hasGlobalQueryClient()) {
85
- getGlobalQueryClient().invalidateQueries({
86
- queryKey: creditsQueryKeys.user(userId),
87
- });
88
- }
89
- } catch (error) {
90
- if (typeof __DEV__ !== "undefined" && __DEV__) {
91
- console.error("[CreditService] Deduct error:", error);
92
- }
93
- }
94
- },
95
-
96
- refundCredits: async (): Promise<void> => {
97
- // No-op for now
98
- },
99
-
100
- calculateCost: (): number => 1,
101
- };
102
- }
103
-
104
- /**
105
- * Creates a paywall service implementation
106
- */
107
- export function createPaywallService(): IPaywallService {
108
- return {
109
- showPaywall: () => paywallControl.open(),
110
- };
111
- }