@umituz/react-native-subscription 2.27.65 → 2.27.67
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 +3 -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/domains/subscription/infrastructure/managers/SubscriptionInternalState.ts +12 -0
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +110 -0
- 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 +68 -0
- package/src/{infrastructure/services → domains/trial/core}/TrialTypes.ts +1 -1
- package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +30 -0
- package/src/presentation/components/details/PremiumStatusBadge.tsx +2 -2
- package/src/presentation/hooks/index.ts +11 -11
- package/src/shared/infrastructure/SubscriptionEventBus.ts +51 -0
- package/src/utils/packageTypeDetector.ts +13 -18
- package/src/application/README.md +0 -50
- package/src/domain/entities/README.md +0 -50
- package/src/domain/entities/SubscriptionStatus.test.ts +0 -105
- 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/useRevenueCat.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/PurchaseHandler.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/RestoreHandler.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatInitializer.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatService.ts +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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CustomerInfo } from "react-native-purchases";
|
|
2
|
+
import type { RevenueCatData } from "../core/RevenueCatData";
|
|
3
|
+
import { type PeriodType, type PurchaseSource } from "../core/SubscriptionConstants";
|
|
4
|
+
import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryProvider";
|
|
5
|
+
import { extractRevenueCatData } from "./SubscriptionSyncUtils";
|
|
6
|
+
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Service to synchronize RevenueCat state with Firestore.
|
|
10
|
+
* Acts as a subscriber/handler for subscription events.
|
|
11
|
+
*/
|
|
12
|
+
export class SubscriptionSyncService {
|
|
13
|
+
constructor(private entitlementId: string) {}
|
|
14
|
+
|
|
15
|
+
async handlePurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource) {
|
|
16
|
+
try {
|
|
17
|
+
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
18
|
+
await getCreditsRepository().initializeCredits(userId, `purchase_${productId}_${Date.now()}`, productId, source, revenueCatData);
|
|
19
|
+
|
|
20
|
+
// Notify listeners via Event Bus
|
|
21
|
+
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
22
|
+
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, { userId, productId });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (__DEV__) console.error('[SubscriptionSyncService] Credits init failed:', error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async handleRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
|
|
29
|
+
try {
|
|
30
|
+
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
31
|
+
revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
|
|
32
|
+
await getCreditsRepository().initializeCredits(userId, `renewal_${productId}_${Date.now()}`, productId, "renewal", revenueCatData);
|
|
33
|
+
|
|
34
|
+
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
35
|
+
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.RENEWAL_DETECTED, { userId, productId });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (__DEV__) console.error('[SubscriptionSyncService] Renewal credits init failed:', error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async handlePremiumStatusChanged(
|
|
42
|
+
userId: string,
|
|
43
|
+
isPremium: boolean,
|
|
44
|
+
productId?: string,
|
|
45
|
+
expiresAt?: string,
|
|
46
|
+
willRenew?: boolean,
|
|
47
|
+
periodType?: PeriodType
|
|
48
|
+
) {
|
|
49
|
+
try {
|
|
50
|
+
if (!isPremium && productId) {
|
|
51
|
+
await getCreditsRepository().syncExpiredStatus(userId);
|
|
52
|
+
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const revenueCatData: RevenueCatData = {
|
|
57
|
+
expirationDate: expiresAt ?? null,
|
|
58
|
+
willRenew: willRenew ?? false,
|
|
59
|
+
isPremium,
|
|
60
|
+
periodType
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await getCreditsRepository().initializeCredits(userId, `status_sync_${Date.now()}`, productId, "settings", revenueCatData);
|
|
64
|
+
|
|
65
|
+
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
66
|
+
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED, { userId, isPremium });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (__DEV__) console.error('[SubscriptionSyncService] Premium status sync failed:', error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CustomerInfo } from "react-native-purchases";
|
|
2
|
+
import type { RevenueCatData } from "../../domain/types/RevenueCatData";
|
|
3
|
+
import { type PeriodType } from "../../domain/entities/SubscriptionConstants";
|
|
4
|
+
|
|
5
|
+
/** Extract RevenueCat data from CustomerInfo (Single Source of Truth) */
|
|
6
|
+
export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId: string): RevenueCatData => {
|
|
7
|
+
const entitlement = customerInfo.entitlements.active[entitlementId]
|
|
8
|
+
?? customerInfo.entitlements.all[entitlementId];
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
expirationDate: entitlement?.expirationDate ?? customerInfo.latestExpirationDate ?? null,
|
|
12
|
+
willRenew: entitlement?.willRenew ?? false,
|
|
13
|
+
originalTransactionId: entitlement?.originalPurchaseDate ?? undefined,
|
|
14
|
+
periodType: entitlement?.periodType as PeriodType | undefined,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -5,6 +5,11 @@ import {
|
|
|
5
5
|
type PeriodType,
|
|
6
6
|
type SubscriptionStatusType
|
|
7
7
|
} from "./SubscriptionConstants";
|
|
8
|
+
import {
|
|
9
|
+
InactiveStatusHandler,
|
|
10
|
+
TrialStatusHandler,
|
|
11
|
+
ActiveStatusHandler
|
|
12
|
+
} from "./SubscriptionStatusHandlers";
|
|
8
13
|
|
|
9
14
|
export {
|
|
10
15
|
SUBSCRIPTION_STATUS,
|
|
@@ -21,9 +26,7 @@ export interface SubscriptionStatus {
|
|
|
21
26
|
customerId?: string | null;
|
|
22
27
|
syncedAt?: string | null;
|
|
23
28
|
status?: SubscriptionStatusType;
|
|
24
|
-
/** RevenueCat period type: NORMAL, INTRO, or TRIAL */
|
|
25
29
|
periodType?: PeriodType;
|
|
26
|
-
/** Whether user is currently in trial period */
|
|
27
30
|
isTrialing?: boolean;
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -40,7 +43,6 @@ export const createDefaultSubscriptionStatus = (): SubscriptionStatus => ({
|
|
|
40
43
|
export const isSubscriptionValid = (status: SubscriptionStatus | null): boolean => {
|
|
41
44
|
if (!status || !status.isPremium) return false;
|
|
42
45
|
if (!status.expiresAt) return true; // Lifetime
|
|
43
|
-
|
|
44
46
|
return timezoneService.isFuture(new Date(status.expiresAt));
|
|
45
47
|
};
|
|
46
48
|
|
|
@@ -49,7 +51,6 @@ export const calculateDaysRemaining = (expiresAt: string | null): number | null
|
|
|
49
51
|
return timezoneService.getDaysUntil(new Date(expiresAt));
|
|
50
52
|
};
|
|
51
53
|
|
|
52
|
-
/** Subscription status resolver input */
|
|
53
54
|
export interface StatusResolverInput {
|
|
54
55
|
isPremium: boolean;
|
|
55
56
|
willRenew?: boolean;
|
|
@@ -57,24 +58,15 @@ export interface StatusResolverInput {
|
|
|
57
58
|
periodType?: PeriodType;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// Singleton Chain Instance
|
|
62
|
+
const inactiveHandler = new InactiveStatusHandler();
|
|
63
|
+
inactiveHandler
|
|
64
|
+
.setNext(new TrialStatusHandler())
|
|
65
|
+
.setNext(new ActiveStatusHandler());
|
|
66
|
+
|
|
60
67
|
/**
|
|
61
|
-
* Resolves subscription status
|
|
62
|
-
* Single source of truth for status determination logic
|
|
68
|
+
* Resolves subscription status using Chain of Responsibility Pattern.
|
|
63
69
|
*/
|
|
64
70
|
export const resolveSubscriptionStatus = (input: StatusResolverInput): SubscriptionStatusType => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!isPremium || isExpired) {
|
|
68
|
-
return isExpired ? SUBSCRIPTION_STATUS.EXPIRED : SUBSCRIPTION_STATUS.NONE;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const isTrial = periodType === PERIOD_TYPE.TRIAL;
|
|
72
|
-
const isCanceled = willRenew === false;
|
|
73
|
-
|
|
74
|
-
if (isTrial) {
|
|
75
|
-
return isCanceled ? SUBSCRIPTION_STATUS.TRIAL_CANCELED : SUBSCRIPTION_STATUS.TRIAL;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return isCanceled ? SUBSCRIPTION_STATUS.CANCELED : SUBSCRIPTION_STATUS.ACTIVE;
|
|
71
|
+
return inactiveHandler.handle(input);
|
|
79
72
|
};
|
|
80
|
-
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SUBSCRIPTION_STATUS,
|
|
3
|
+
PERIOD_TYPE,
|
|
4
|
+
type SubscriptionStatusType
|
|
5
|
+
} from "./SubscriptionConstants";
|
|
6
|
+
import type { StatusResolverInput } from "./SubscriptionStatus";
|
|
7
|
+
|
|
8
|
+
export abstract class BaseStatusHandler {
|
|
9
|
+
protected next?: BaseStatusHandler;
|
|
10
|
+
|
|
11
|
+
setNext(handler: BaseStatusHandler): BaseStatusHandler {
|
|
12
|
+
this.next = handler;
|
|
13
|
+
return handler;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
abstract handle(input: StatusResolverInput): SubscriptionStatusType;
|
|
17
|
+
|
|
18
|
+
protected nextOrFallback(input: StatusResolverInput, fallback: SubscriptionStatusType): SubscriptionStatusType {
|
|
19
|
+
return this.next ? this.next.handle(input) : fallback;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Handles Expired or No-Premium cases */
|
|
24
|
+
export class InactiveStatusHandler extends BaseStatusHandler {
|
|
25
|
+
handle(input: StatusResolverInput): SubscriptionStatusType {
|
|
26
|
+
if (!input.isPremium || input.isExpired) {
|
|
27
|
+
return input.isExpired ? SUBSCRIPTION_STATUS.EXPIRED : SUBSCRIPTION_STATUS.NONE;
|
|
28
|
+
}
|
|
29
|
+
return this.nextOrFallback(input, SUBSCRIPTION_STATUS.NONE);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Handles Trial-related states */
|
|
34
|
+
export class TrialStatusHandler extends BaseStatusHandler {
|
|
35
|
+
handle(input: StatusResolverInput): SubscriptionStatusType {
|
|
36
|
+
if (input.periodType === PERIOD_TYPE.TRIAL) {
|
|
37
|
+
return input.willRenew === false ? SUBSCRIPTION_STATUS.TRIAL_CANCELED : SUBSCRIPTION_STATUS.TRIAL;
|
|
38
|
+
}
|
|
39
|
+
return this.nextOrFallback(input, SUBSCRIPTION_STATUS.ACTIVE);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Handles Canceled-Active states */
|
|
44
|
+
export class ActiveStatusHandler extends BaseStatusHandler {
|
|
45
|
+
handle(input: StatusResolverInput): SubscriptionStatusType {
|
|
46
|
+
if (input.willRenew === false) {
|
|
47
|
+
return SUBSCRIPTION_STATUS.CANCELED;
|
|
48
|
+
}
|
|
49
|
+
return SUBSCRIPTION_STATUS.ACTIVE;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Handler
|
|
3
|
+
* Handles operations: fetch, purchase, restore
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PurchasesPackage, CustomerInfo } from "react-native-purchases";
|
|
7
|
+
import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
|
|
8
|
+
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
9
|
+
import { PurchaseStatusResolver, type PremiumStatus } from "./PurchaseStatusResolver";
|
|
10
|
+
|
|
11
|
+
export interface RestoreResultInfo {
|
|
12
|
+
success: boolean;
|
|
13
|
+
productId: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class PackageHandler {
|
|
17
|
+
constructor(
|
|
18
|
+
private service: IRevenueCatService | null,
|
|
19
|
+
private entitlementId: string
|
|
20
|
+
) { }
|
|
21
|
+
|
|
22
|
+
setService = (service: IRevenueCatService | null) => { this.service = service; };
|
|
23
|
+
|
|
24
|
+
async fetchPackages(): Promise<PurchasesPackage[]> {
|
|
25
|
+
if (!this.service?.isInitialized()) return [];
|
|
26
|
+
try {
|
|
27
|
+
const offering = await this.service.fetchOfferings();
|
|
28
|
+
return offering?.availablePackages ?? [];
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (__DEV__) console.error('[PackageHandler] fetchOfferings failed:', error);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
|
|
36
|
+
if (!this.service?.isInitialized()) return false;
|
|
37
|
+
try {
|
|
38
|
+
const result = await this.service.purchasePackage(pkg, userId);
|
|
39
|
+
return result.success;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (__DEV__) console.error('[PackageHandler] Purchase failed:', error);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async restore(userId: string): Promise<RestoreResultInfo> {
|
|
47
|
+
if (!this.service?.isInitialized()) return { success: false, productId: null };
|
|
48
|
+
try {
|
|
49
|
+
const result = await this.service.restorePurchases(userId);
|
|
50
|
+
let productId: string | null = null;
|
|
51
|
+
if (result.success && result.customerInfo) {
|
|
52
|
+
const entitlement = getPremiumEntitlement(result.customerInfo, this.entitlementId);
|
|
53
|
+
if (entitlement) productId = entitlement.productIdentifier;
|
|
54
|
+
}
|
|
55
|
+
return { success: result.success, productId };
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (__DEV__) console.error('[PackageHandler] Restore failed:', error);
|
|
58
|
+
return { success: false, productId: null };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
checkPremiumStatusFromInfo(customerInfo: CustomerInfo): PremiumStatus {
|
|
63
|
+
return PurchaseStatusResolver.resolve(customerInfo, this.entitlementId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type { PremiumStatus };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CustomerInfo } from "react-native-purchases";
|
|
2
|
+
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
3
|
+
|
|
4
|
+
export interface PremiumStatus {
|
|
5
|
+
isPremium: boolean;
|
|
6
|
+
expirationDate: Date | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class PurchaseStatusResolver {
|
|
10
|
+
static resolve(customerInfo: CustomerInfo, entitlementId: string): PremiumStatus {
|
|
11
|
+
const entitlement = getPremiumEntitlement(customerInfo, entitlementId);
|
|
12
|
+
|
|
13
|
+
if (entitlement) {
|
|
14
|
+
return {
|
|
15
|
+
isPremium: true,
|
|
16
|
+
expirationDate: entitlement.expirationDate
|
|
17
|
+
? new Date(entitlement.expirationDate)
|
|
18
|
+
: null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
isPremium: false,
|
|
24
|
+
expirationDate: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { UserIdProvider } from "../utils/UserIdProvider";
|
|
2
|
+
import { InitializationCache } from "../utils/InitializationCache";
|
|
3
|
+
|
|
4
|
+
export class SubscriptionInternalState {
|
|
5
|
+
public userIdProvider = new UserIdProvider();
|
|
6
|
+
public initCache = new InitializationCache();
|
|
7
|
+
|
|
8
|
+
reset() {
|
|
9
|
+
this.userIdProvider.reset();
|
|
10
|
+
this.initCache.reset();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Manager
|
|
3
|
+
* Facade for subscription operations. Coordinates state and operations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
+
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
8
|
+
import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
|
|
9
|
+
import { initializeRevenueCatService, getRevenueCatService } from "../services/RevenueCatService";
|
|
10
|
+
import { PackageHandler } from "../handlers/PackageHandler";
|
|
11
|
+
import type { PremiumStatus, RestoreResultInfo } from "../handlers/PackageHandler";
|
|
12
|
+
import { SubscriptionInternalState } from "./SubscriptionInternalState";
|
|
13
|
+
|
|
14
|
+
export interface SubscriptionManagerConfig {
|
|
15
|
+
config: RevenueCatConfig;
|
|
16
|
+
apiKey: string;
|
|
17
|
+
getAnonymousUserId?: () => Promise<string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class SubscriptionManagerImpl {
|
|
21
|
+
private managerConfig: SubscriptionManagerConfig | null = null;
|
|
22
|
+
private serviceInstance: IRevenueCatService | null = null;
|
|
23
|
+
private state = new SubscriptionInternalState();
|
|
24
|
+
private packageHandler: PackageHandler | null = null;
|
|
25
|
+
|
|
26
|
+
configure(config: SubscriptionManagerConfig): void {
|
|
27
|
+
this.managerConfig = config;
|
|
28
|
+
this.packageHandler = new PackageHandler(null, config.config.entitlementIdentifier);
|
|
29
|
+
if (config.getAnonymousUserId) this.state.userIdProvider.configure(config.getAnonymousUserId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private ensureConfigured(): void {
|
|
33
|
+
if (!this.managerConfig || !this.packageHandler) throw new Error("SubscriptionManager not configured");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async initialize(userId?: string): Promise<boolean> {
|
|
37
|
+
this.ensureConfigured();
|
|
38
|
+
const effectiveUserId = userId || (await this.state.userIdProvider.getOrCreateAnonymousUserId());
|
|
39
|
+
const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(effectiveUserId);
|
|
40
|
+
|
|
41
|
+
if (!shouldInit && existingPromise) return existingPromise;
|
|
42
|
+
|
|
43
|
+
const promise = (async () => {
|
|
44
|
+
await initializeRevenueCatService(this.managerConfig!.config);
|
|
45
|
+
this.serviceInstance = getRevenueCatService();
|
|
46
|
+
if (!this.serviceInstance) return false;
|
|
47
|
+
this.packageHandler!.setService(this.serviceInstance);
|
|
48
|
+
const result = await this.serviceInstance.initialize(effectiveUserId);
|
|
49
|
+
return result.success;
|
|
50
|
+
})();
|
|
51
|
+
|
|
52
|
+
this.state.initCache.setPromise(promise, effectiveUserId);
|
|
53
|
+
return promise;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isInitializedForUser(userId: string): boolean {
|
|
57
|
+
return this.serviceInstance?.isInitialized() === true && this.state.initCache.getCurrentUserId() === userId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getPackages(): Promise<PurchasesPackage[]> {
|
|
61
|
+
this.ensureConfigured();
|
|
62
|
+
if (!this.serviceInstance) {
|
|
63
|
+
this.serviceInstance = getRevenueCatService();
|
|
64
|
+
this.packageHandler!.setService(this.serviceInstance);
|
|
65
|
+
}
|
|
66
|
+
return this.packageHandler!.fetchPackages();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
|
|
70
|
+
this.ensureConfigured();
|
|
71
|
+
const userId = this.state.initCache.getCurrentUserId();
|
|
72
|
+
if (!userId) return false;
|
|
73
|
+
return this.packageHandler!.purchase(pkg, userId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async restore(): Promise<RestoreResultInfo> {
|
|
77
|
+
this.ensureConfigured();
|
|
78
|
+
const userId = this.state.initCache.getCurrentUserId();
|
|
79
|
+
if (!userId) return { success: false, productId: null };
|
|
80
|
+
return this.packageHandler!.restore(userId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async checkPremiumStatus(): Promise<PremiumStatus> {
|
|
84
|
+
this.ensureConfigured();
|
|
85
|
+
const userId = this.state.initCache.getCurrentUserId();
|
|
86
|
+
if (!userId) return { isPremium: false, expirationDate: null };
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const customerInfo = await this.serviceInstance?.getCustomerInfo();
|
|
90
|
+
if (customerInfo) return this.packageHandler!.checkPremiumStatusFromInfo(customerInfo);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
return { isPremium: false, expirationDate: null };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async reset(): Promise<void> {
|
|
98
|
+
if (this.serviceInstance) await this.serviceInstance.reset();
|
|
99
|
+
this.state.reset();
|
|
100
|
+
this.serviceInstance = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Helper status checks
|
|
104
|
+
isConfigured = () => !!this.managerConfig;
|
|
105
|
+
isInitialized = () => this.serviceInstance?.isInitialized() ?? false;
|
|
106
|
+
getEntitlementId = () => this.managerConfig?.config.entitlementIdentifier || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const SubscriptionManager = new SubscriptionManagerImpl();
|
|
110
|
+
export type { PremiumStatus };
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { useCallback } from 'react';
|
|
9
9
|
import type { PurchasesPackage } from 'react-native-purchases';
|
|
10
|
-
import type { UserCredits } from '../../
|
|
11
|
-
import { useCredits } from '
|
|
10
|
+
import type { UserCredits } from '../../credits/core/Credits';
|
|
11
|
+
import { useCredits } from '../../credits/presentation/useCredits';
|
|
12
12
|
import { useSubscriptionStatus } from './useSubscriptionStatus';
|
|
13
13
|
import {
|
|
14
14
|
useSubscriptionPackages,
|
|
15
15
|
usePurchasePackage,
|
|
16
16
|
useRestorePurchase,
|
|
17
|
-
} from '
|
|
17
|
+
} from '../infrastructure/hooks/useSubscriptionQueries';
|
|
18
18
|
import { usePaywallVisibility } from './usePaywallVisibility';
|
|
19
19
|
|
|
20
20
|
export interface UsePremiumResult {
|
|
@@ -23,6 +23,7 @@ export interface UsePremiumResult {
|
|
|
23
23
|
packages: PurchasesPackage[];
|
|
24
24
|
credits: UserCredits | null;
|
|
25
25
|
showPaywall: boolean;
|
|
26
|
+
isSyncing: boolean;
|
|
26
27
|
purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
27
28
|
restorePurchase: () => Promise<boolean>;
|
|
28
29
|
setShowPaywall: (show: boolean) => void;
|
|
@@ -40,8 +41,9 @@ export const usePremium = (): UsePremiumResult => {
|
|
|
40
41
|
const restoreMutation = useRestorePurchase();
|
|
41
42
|
|
|
42
43
|
const { showPaywall, setShowPaywall, closePaywall, openPaywall } = usePaywallVisibility();
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
const isPremium = subscriptionActive;
|
|
46
|
+
const isSyncing = subscriptionActive && credits !== null && !credits.isPremium;
|
|
45
47
|
|
|
46
48
|
const handlePurchase = useCallback(
|
|
47
49
|
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
@@ -84,6 +86,7 @@ export const usePremium = (): UsePremiumResult => {
|
|
|
84
86
|
packages,
|
|
85
87
|
credits,
|
|
86
88
|
showPaywall,
|
|
89
|
+
isSyncing,
|
|
87
90
|
purchasePackage: handlePurchase,
|
|
88
91
|
restorePurchase: handleRestore,
|
|
89
92
|
setShowPaywall,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TrialEligibilityResult, DeviceTrialRecord } from "./TrialTypes";
|
|
2
|
+
|
|
3
|
+
export class TrialEligibilityService {
|
|
4
|
+
static check(
|
|
5
|
+
userId: string | undefined,
|
|
6
|
+
deviceId: string,
|
|
7
|
+
record: DeviceTrialRecord | null
|
|
8
|
+
): TrialEligibilityResult {
|
|
9
|
+
if (!record) {
|
|
10
|
+
return { eligible: true, deviceId };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { hasUsedTrial, trialInProgress, userIds = [] } = record;
|
|
14
|
+
|
|
15
|
+
if (userId && userIds.includes(userId)) {
|
|
16
|
+
return { eligible: false, reason: "user_already_used", deviceId };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (hasUsedTrial || trialInProgress) {
|
|
20
|
+
return { eligible: false, reason: "already_used", deviceId };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { eligible: true, deviceId };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trial Service - Facade for device-based trial tracking
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { arrayUnion, serverTimestamp } from "firebase/firestore";
|
|
6
|
+
import { PersistentDeviceIdService } from "@umituz/react-native-design-system";
|
|
7
|
+
import { DeviceTrialRepository } from "../infrastructure/DeviceTrialRepository";
|
|
8
|
+
import { TrialEligibilityService } from "./TrialEligibilityService";
|
|
9
|
+
import type { TrialEligibilityResult } from "../core/TrialTypes";
|
|
10
|
+
|
|
11
|
+
const repository = new DeviceTrialRepository();
|
|
12
|
+
|
|
13
|
+
export const getDeviceId = () => PersistentDeviceIdService.getDeviceId();
|
|
14
|
+
|
|
15
|
+
export async function checkTrialEligibility(userId?: string, deviceId?: string): Promise<TrialEligibilityResult> {
|
|
16
|
+
try {
|
|
17
|
+
const id = deviceId || await getDeviceId();
|
|
18
|
+
const record = await repository.getRecord(id);
|
|
19
|
+
return TrialEligibilityService.check(userId, id, record);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (__DEV__) console.error("[TrialService] Eligibility check error:", error);
|
|
22
|
+
return { eligible: false, reason: "error" };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function recordTrialStart(userId: string, deviceId?: string): Promise<boolean> {
|
|
27
|
+
try {
|
|
28
|
+
const id = deviceId || await getDeviceId();
|
|
29
|
+
return await repository.saveRecord(id, {
|
|
30
|
+
deviceId: id,
|
|
31
|
+
trialInProgress: true,
|
|
32
|
+
trialStartedAt: serverTimestamp() as any,
|
|
33
|
+
lastUserId: userId,
|
|
34
|
+
userIds: arrayUnion(userId) as any,
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (__DEV__) console.error("[TrialService] Record trial error:", error);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function recordTrialEnd(deviceId?: string): Promise<boolean> {
|
|
43
|
+
try {
|
|
44
|
+
const id = deviceId || await getDeviceId();
|
|
45
|
+
return await repository.saveRecord(id, {
|
|
46
|
+
hasUsedTrial: true,
|
|
47
|
+
trialInProgress: false,
|
|
48
|
+
trialEndedAt: serverTimestamp() as any,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (__DEV__) console.error("[TrialService] Record trial end error:", error);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function recordTrialConversion(deviceId?: string): Promise<boolean> {
|
|
57
|
+
try {
|
|
58
|
+
const id = deviceId || await getDeviceId();
|
|
59
|
+
return await repository.saveRecord(id, {
|
|
60
|
+
hasUsedTrial: true,
|
|
61
|
+
trialInProgress: false,
|
|
62
|
+
trialConvertedAt: serverTimestamp() as any,
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (__DEV__) console.error("[TrialService] Record conversion error:", error);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -26,6 +26,6 @@ export interface DeviceTrialRecord {
|
|
|
26
26
|
/** Trial eligibility result */
|
|
27
27
|
export interface TrialEligibilityResult {
|
|
28
28
|
eligible: boolean;
|
|
29
|
-
reason?: "already_used" | "device_not_found" | "error";
|
|
29
|
+
reason?: "already_used" | "device_not_found" | "error" | "user_already_used";
|
|
30
30
|
deviceId?: string;
|
|
31
31
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { doc, getDoc, setDoc, serverTimestamp, arrayUnion, type Firestore } from "firebase/firestore";
|
|
2
|
+
import { getFirestore } from "@umituz/react-native-firebase";
|
|
3
|
+
import type { DeviceTrialRecord } from "./TrialTypes";
|
|
4
|
+
|
|
5
|
+
const DEVICE_TRIALS_COLLECTION = "device_trials";
|
|
6
|
+
|
|
7
|
+
export class DeviceTrialRepository {
|
|
8
|
+
private get db(): Firestore | null {
|
|
9
|
+
return getFirestore();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async getRecord(deviceId: string): Promise<DeviceTrialRecord | null> {
|
|
13
|
+
if (!this.db) return null;
|
|
14
|
+
const snap = await getDoc(doc(this.db, DEVICE_TRIALS_COLLECTION, deviceId));
|
|
15
|
+
return snap.exists() ? snap.data() as DeviceTrialRecord : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async saveRecord(deviceId: string, data: Partial<DeviceTrialRecord>): Promise<boolean> {
|
|
19
|
+
if (!this.db) return false;
|
|
20
|
+
const ref = doc(this.db, DEVICE_TRIALS_COLLECTION, deviceId);
|
|
21
|
+
await setDoc(ref, { ...data, updatedAt: serverTimestamp() }, { merge: true });
|
|
22
|
+
|
|
23
|
+
// Ensure createdAt exists
|
|
24
|
+
const snap = await getDoc(ref);
|
|
25
|
+
if (!snap.data()?.createdAt) {
|
|
26
|
+
await setDoc(ref, { createdAt: serverTimestamp() }, { merge: true });
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -33,14 +33,14 @@ export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
|
|
|
33
33
|
}) => {
|
|
34
34
|
const tokens = useAppDesignTokens();
|
|
35
35
|
|
|
36
|
-
const labels: Record<SubscriptionStatusType, string> = {
|
|
36
|
+
const labels: Record<SubscriptionStatusType, string> = useMemo(() => ({
|
|
37
37
|
[SUBSCRIPTION_STATUS.ACTIVE]: activeLabel,
|
|
38
38
|
[SUBSCRIPTION_STATUS.TRIAL]: activeLabel,
|
|
39
39
|
[SUBSCRIPTION_STATUS.TRIAL_CANCELED]: trialCanceledLabel ?? canceledLabel,
|
|
40
40
|
[SUBSCRIPTION_STATUS.EXPIRED]: expiredLabel,
|
|
41
41
|
[SUBSCRIPTION_STATUS.NONE]: noneLabel,
|
|
42
42
|
[SUBSCRIPTION_STATUS.CANCELED]: canceledLabel,
|
|
43
|
-
};
|
|
43
|
+
}), [activeLabel, trialCanceledLabel, canceledLabel, expiredLabel, noneLabel]);
|
|
44
44
|
|
|
45
45
|
const backgroundColor = useMemo(() => {
|
|
46
46
|
const colors: Record<SubscriptionStatusType, string> = {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export * from "
|
|
2
|
-
export * from "
|
|
3
|
-
export * from "
|
|
4
|
-
export * from "
|
|
5
|
-
export * from "
|
|
6
|
-
export * from "
|
|
7
|
-
export * from "
|
|
8
|
-
export * from "
|
|
9
|
-
export * from "
|
|
10
|
-
export * from "
|
|
11
|
-
export * from "
|
|
1
|
+
export * from "../../domains/subscription/presentation/useAuthAwarePurchase";
|
|
2
|
+
export * from "../../domains/subscription/presentation/useAuthSubscriptionSync";
|
|
3
|
+
export * from "../../domains/subscription/presentation/useSavedPurchaseAutoExecution";
|
|
4
|
+
export * from "../../domains/credits/presentation/useCredits";
|
|
5
|
+
export * from "../../domains/credits/presentation/useDeductCredit";
|
|
6
|
+
export * from "../../domains/subscription/presentation/useFeatureGate";
|
|
7
|
+
export * from "../../domains/subscription/presentation/usePaywallVisibility";
|
|
8
|
+
export * from "../../domains/subscription/presentation/usePremium";
|
|
9
|
+
export * from "../../domains/subscription/presentation/usePremiumGate";
|
|
10
|
+
export * from "../../domains/subscription/presentation/useSubscriptionSettingsConfig";
|
|
11
|
+
export * from "../../domains/subscription/presentation/useSubscriptionStatus";
|
|
12
12
|
export * from "./feedback/usePaywallFeedback";
|
|
13
13
|
export * from "./feedback/useFeedbackSubmit";
|