@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.
- package/package.json +1 -1
- package/src/domains/credits/application/CreditLimitCalculator.ts +17 -0
- package/src/domains/credits/application/CreditsInitializer.ts +85 -0
- package/src/domains/credits/application/DeductCreditsCommand.ts +52 -0
- package/src/domains/credits/application/PurchaseMetadataGenerator.ts +59 -0
- package/src/domains/credits/application/credit-strategies/CreditAllocationContext.ts +35 -0
- package/src/domains/credits/application/credit-strategies/ICreditStrategy.ts +18 -0
- package/src/domains/credits/application/credit-strategies/StandardPurchaseCreditStrategy.ts +16 -0
- package/src/domains/credits/application/credit-strategies/SyncCreditStrategy.ts +15 -0
- package/src/domains/credits/application/credit-strategies/TrialCreditStrategy.ts +18 -0
- package/src/{infrastructure/mappers → domains/credits/core}/CreditsMapper.ts +4 -4
- package/src/domains/credits/infrastructure/CreditsRepository.ts +102 -0
- package/src/{presentation/hooks → domains/credits/presentation}/useCredits.ts +21 -4
- package/src/domains/subscription/application/SubscriptionAuthListener.ts +26 -0
- package/src/domains/subscription/application/SubscriptionInitializer.ts +77 -0
- package/src/{infrastructure/services → domains/subscription/application}/SubscriptionInitializerTypes.ts +21 -1
- package/src/domains/subscription/application/SubscriptionSyncService.ts +71 -0
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +16 -0
- package/src/{revenuecat/domain/value-objects → domains/subscription/core}/RevenueCatConfig.ts +1 -1
- package/src/{domain/types → domains/subscription/core}/RevenueCatData.ts +1 -1
- package/src/{domain/entities → domains/subscription/core}/SubscriptionStatus.ts +13 -21
- package/src/domains/subscription/core/SubscriptionStatusHandlers.ts +51 -0
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +67 -0
- package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +27 -0
- package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRevenueCat.ts +1 -1
- package/src/domains/subscription/infrastructure/managers/SubscriptionInternalState.ts +12 -0
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +110 -0
- package/src/{revenuecat → domains/subscription}/infrastructure/services/PurchaseHandler.ts +1 -1
- package/src/{revenuecat → domains/subscription}/infrastructure/services/RestoreHandler.ts +1 -1
- package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatInitializer.ts +1 -1
- package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatService.ts +2 -2
- package/src/{presentation/hooks → domains/subscription/presentation}/usePremium.ts +7 -4
- package/src/domains/trial/application/TrialEligibilityService.ts +25 -0
- package/src/domains/trial/application/TrialService.ts +69 -0
- package/src/{infrastructure/services → domains/trial/core}/TrialTypes.ts +1 -1
- package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +30 -0
- package/src/index.ts +28 -59
- package/src/init/createSubscriptionInitModule.ts +1 -1
- package/src/presentation/components/details/PremiumStatusBadge.tsx +1 -3
- package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +1 -1
- package/src/presentation/hooks/index.ts +11 -11
- package/src/shared/application/ports/IRevenueCatService.ts +32 -0
- package/src/shared/infrastructure/SubscriptionEventBus.ts +51 -0
- package/src/application/README.md +0 -50
- package/src/domain/README.md +0 -54
- package/src/domain/entities/README.md +0 -50
- package/src/domain/entities/SubscriptionStatus.test.ts +0 -149
- package/src/domain/errors/README.md +0 -53
- package/src/domain/value-objects/README.md +0 -50
- package/src/infrastructure/README.md +0 -55
- package/src/infrastructure/mappers/README.md +0 -21
- package/src/infrastructure/models/README.md +0 -26
- package/src/infrastructure/repositories/CreditsRepository.ts +0 -132
- package/src/infrastructure/repositories/README.md +0 -99
- package/src/infrastructure/services/CreditsInitializer.ts +0 -170
- package/src/infrastructure/services/README.md +0 -99
- package/src/infrastructure/services/SubscriptionInitializer.ts +0 -176
- package/src/infrastructure/services/SubscriptionService.ts +0 -133
- package/src/infrastructure/services/TrialService.ts +0 -197
- package/src/infrastructure/services/app-service-helpers.ts +0 -111
- package/src/revenuecat/README.md +0 -104
- package/src/revenuecat/application/README.md +0 -43
- package/src/revenuecat/application/ports/IRevenueCatService.ts +0 -76
- package/src/revenuecat/application/ports/README.md +0 -41
- package/src/revenuecat/domain/README.md +0 -48
- package/src/revenuecat/domain/constants/README.md +0 -41
- package/src/revenuecat/domain/entities/README.md +0 -42
- package/src/revenuecat/domain/errors/README.md +0 -53
- package/src/revenuecat/domain/types/README.md +0 -41
- package/src/revenuecat/domain/value-objects/README.md +0 -41
- package/src/revenuecat/index.ts +0 -13
- package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +0 -161
- package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +0 -165
- package/src/revenuecat/presentation/README.md +0 -42
- /package/src/{domain/entities → domains/credits/core}/Credits.ts +0 -0
- /package/src/{infrastructure/models → domains/credits/core}/UserCreditsDocument.ts +0 -0
- /package/src/{infrastructure/repositories → domains/credits/infrastructure}/CreditsRepositoryProvider.ts +0 -0
- /package/src/{presentation/hooks → domains/credits/presentation}/useDeductCredit.ts +0 -0
- /package/src/{revenuecat/domain/constants → domains/subscription/core}/RevenueCatConstants.ts +0 -0
- /package/src/{revenuecat/domain/errors → domains/subscription/core}/RevenueCatError.ts +0 -0
- /package/src/{revenuecat/domain/types → domains/subscription/core}/RevenueCatTypes.ts +0 -0
- /package/src/{domain/entities → domains/subscription/core}/SubscriptionConstants.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/config/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/handlers/README.md +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/README.md +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/subscriptionQueryKeys.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useCustomerInfo.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useInitializeSubscription.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/usePaywallFlow.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/usePurchasePackage.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRestorePurchase.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRevenueCatTrialEligibility.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useSubscriptionPackages.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useSubscriptionQueries.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/managers/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/CustomerInfoListenerManager.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/OfferingsFetcher.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/ServiceStateManager.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/ApiKeyResolver.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/InitializationCache.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/PremiumStatusSyncer.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/RenewalDetector.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/UserIdProvider.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useAuthAwarePurchase.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useAuthSubscriptionSync.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useFeatureGate.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/usePaywallVisibility.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/usePremiumGate.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useSavedPurchaseAutoExecution.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionSettingsConfig.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionSettingsConfig.utils.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionStatus.ts +0 -0
- /package/src/{infrastructure/services → shared/application}/ActivationHandler.ts +0 -0
- /package/src/{infrastructure/services → shared/application}/FeedbackService.ts +0 -0
- /package/src/{application → shared/application}/ports/ISubscriptionRepository.ts +0 -0
- /package/src/{application → shared/application}/ports/ISubscriptionService.ts +0 -0
- /package/src/{application → shared/application}/ports/README.md +0 -0
- /package/src/{domain/errors → shared/utils}/InsufficientCreditsError.ts +0 -0
- /package/src/{infrastructure → shared}/utils/Logger.ts +0 -0
- /package/src/{domain/value-objects → shared/utils}/Result.ts +0 -0
- /package/src/{domain/value-objects → shared/utils}/SubscriptionConfig.ts +0 -0
- /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
|
-
}
|