@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.
Files changed (121) hide show
  1. package/package.json +3 -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/domains/subscription/infrastructure/managers/SubscriptionInternalState.ts +12 -0
  26. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +110 -0
  27. package/src/{presentation/hooks → domains/subscription/presentation}/usePremium.ts +7 -4
  28. package/src/domains/trial/application/TrialEligibilityService.ts +25 -0
  29. package/src/domains/trial/application/TrialService.ts +68 -0
  30. package/src/{infrastructure/services → domains/trial/core}/TrialTypes.ts +1 -1
  31. package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +30 -0
  32. package/src/presentation/components/details/PremiumStatusBadge.tsx +2 -2
  33. package/src/presentation/hooks/index.ts +11 -11
  34. package/src/shared/infrastructure/SubscriptionEventBus.ts +51 -0
  35. package/src/utils/packageTypeDetector.ts +13 -18
  36. package/src/application/README.md +0 -50
  37. package/src/domain/entities/README.md +0 -50
  38. package/src/domain/entities/SubscriptionStatus.test.ts +0 -105
  39. package/src/domain/errors/README.md +0 -53
  40. package/src/domain/value-objects/README.md +0 -50
  41. package/src/infrastructure/README.md +0 -55
  42. package/src/infrastructure/mappers/README.md +0 -21
  43. package/src/infrastructure/models/README.md +0 -26
  44. package/src/infrastructure/repositories/CreditsRepository.ts +0 -132
  45. package/src/infrastructure/repositories/README.md +0 -99
  46. package/src/infrastructure/services/CreditsInitializer.ts +0 -170
  47. package/src/infrastructure/services/README.md +0 -99
  48. package/src/infrastructure/services/SubscriptionInitializer.ts +0 -176
  49. package/src/infrastructure/services/SubscriptionService.ts +0 -133
  50. package/src/infrastructure/services/TrialService.ts +0 -197
  51. package/src/infrastructure/services/app-service-helpers.ts +0 -111
  52. package/src/revenuecat/README.md +0 -104
  53. package/src/revenuecat/application/README.md +0 -43
  54. package/src/revenuecat/application/ports/IRevenueCatService.ts +0 -76
  55. package/src/revenuecat/application/ports/README.md +0 -41
  56. package/src/revenuecat/domain/README.md +0 -48
  57. package/src/revenuecat/domain/constants/README.md +0 -41
  58. package/src/revenuecat/domain/entities/README.md +0 -42
  59. package/src/revenuecat/domain/errors/README.md +0 -53
  60. package/src/revenuecat/domain/types/README.md +0 -41
  61. package/src/revenuecat/domain/value-objects/README.md +0 -41
  62. package/src/revenuecat/index.ts +0 -13
  63. package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +0 -161
  64. package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +0 -165
  65. package/src/revenuecat/presentation/README.md +0 -42
  66. /package/src/{domain/entities → domains/credits/core}/Credits.ts +0 -0
  67. /package/src/{infrastructure/models → domains/credits/core}/UserCreditsDocument.ts +0 -0
  68. /package/src/{infrastructure/repositories → domains/credits/infrastructure}/CreditsRepositoryProvider.ts +0 -0
  69. /package/src/{presentation/hooks → domains/credits/presentation}/useDeductCredit.ts +0 -0
  70. /package/src/{revenuecat/domain/constants → domains/subscription/core}/RevenueCatConstants.ts +0 -0
  71. /package/src/{revenuecat/domain/errors → domains/subscription/core}/RevenueCatError.ts +0 -0
  72. /package/src/{revenuecat/domain/types → domains/subscription/core}/RevenueCatTypes.ts +0 -0
  73. /package/src/{domain/entities → domains/subscription/core}/SubscriptionConstants.ts +0 -0
  74. /package/src/{revenuecat → domains/subscription}/infrastructure/README.md +0 -0
  75. /package/src/{revenuecat → domains/subscription}/infrastructure/config/README.md +0 -0
  76. /package/src/{revenuecat → domains/subscription}/infrastructure/handlers/README.md +0 -0
  77. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/README.md +0 -0
  78. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/subscriptionQueryKeys.ts +0 -0
  79. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useCustomerInfo.ts +0 -0
  80. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useInitializeSubscription.ts +0 -0
  81. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/usePaywallFlow.ts +0 -0
  82. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/usePurchasePackage.ts +0 -0
  83. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRestorePurchase.ts +0 -0
  84. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRevenueCat.ts +0 -0
  85. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRevenueCatTrialEligibility.ts +0 -0
  86. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useSubscriptionPackages.ts +0 -0
  87. /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useSubscriptionQueries.ts +0 -0
  88. /package/src/{revenuecat → domains/subscription}/infrastructure/managers/README.md +0 -0
  89. /package/src/{revenuecat → domains/subscription}/infrastructure/services/CustomerInfoListenerManager.ts +0 -0
  90. /package/src/{revenuecat → domains/subscription}/infrastructure/services/OfferingsFetcher.ts +0 -0
  91. /package/src/{revenuecat → domains/subscription}/infrastructure/services/PurchaseHandler.ts +0 -0
  92. /package/src/{revenuecat → domains/subscription}/infrastructure/services/README.md +0 -0
  93. /package/src/{revenuecat → domains/subscription}/infrastructure/services/RestoreHandler.ts +0 -0
  94. /package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatInitializer.ts +0 -0
  95. /package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatService.ts +0 -0
  96. /package/src/{revenuecat → domains/subscription}/infrastructure/services/ServiceStateManager.ts +0 -0
  97. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/ApiKeyResolver.ts +0 -0
  98. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/InitializationCache.ts +0 -0
  99. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/PremiumStatusSyncer.ts +0 -0
  100. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/README.md +0 -0
  101. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/RenewalDetector.ts +0 -0
  102. /package/src/{revenuecat → domains/subscription}/infrastructure/utils/UserIdProvider.ts +0 -0
  103. /package/src/{presentation/hooks → domains/subscription/presentation}/useAuthAwarePurchase.ts +0 -0
  104. /package/src/{presentation/hooks → domains/subscription/presentation}/useAuthSubscriptionSync.ts +0 -0
  105. /package/src/{presentation/hooks → domains/subscription/presentation}/useFeatureGate.ts +0 -0
  106. /package/src/{presentation/hooks → domains/subscription/presentation}/usePaywallVisibility.ts +0 -0
  107. /package/src/{presentation/hooks → domains/subscription/presentation}/usePremiumGate.ts +0 -0
  108. /package/src/{presentation/hooks → domains/subscription/presentation}/useSavedPurchaseAutoExecution.ts +0 -0
  109. /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionSettingsConfig.ts +0 -0
  110. /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionSettingsConfig.utils.ts +0 -0
  111. /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionStatus.ts +0 -0
  112. /package/src/{infrastructure/services → shared/application}/ActivationHandler.ts +0 -0
  113. /package/src/{infrastructure/services → shared/application}/FeedbackService.ts +0 -0
  114. /package/src/{application → shared/application}/ports/ISubscriptionRepository.ts +0 -0
  115. /package/src/{application → shared/application}/ports/ISubscriptionService.ts +0 -0
  116. /package/src/{application → shared/application}/ports/README.md +0 -0
  117. /package/src/{domain/errors → shared/utils}/InsufficientCreditsError.ts +0 -0
  118. /package/src/{infrastructure → shared}/utils/Logger.ts +0 -0
  119. /package/src/{domain/value-objects → shared/utils}/Result.ts +0 -0
  120. /package/src/{domain/value-objects → shared/utils}/SubscriptionConfig.ts +0 -0
  121. /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
+ };
@@ -1,5 +1,5 @@
1
1
  import type { CustomerInfo } from "react-native-purchases";
2
- import type { PurchaseSource } from "../../../domain/entities/Credits";
2
+ import type { PurchaseSource } from "./SubscriptionConstants";
3
3
 
4
4
  export interface RevenueCatConfig {
5
5
  apiKey?: string;
@@ -1,4 +1,4 @@
1
- import type { PeriodType } from "../entities/SubscriptionStatus";
1
+ import type { PeriodType } from "./SubscriptionStatus";
2
2
 
3
3
  /**
4
4
  * RevenueCat subscription data (Single Source of Truth)
@@ -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 from input parameters
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
- const { isPremium, willRenew, isExpired, periodType } = input;
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 '../../domain/entities/Credits';
11
- import { useCredits } from './useCredits';
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 '../../revenuecat/presentation/hooks/useSubscriptionQueries';
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 "./useAuthAwarePurchase";
2
- export * from "./useAuthSubscriptionSync";
3
- export * from "./useSavedPurchaseAutoExecution";
4
- export * from "./useCredits";
5
- export * from "./useDeductCredit";
6
- export * from "./useFeatureGate";
7
- export * from "./usePaywallVisibility";
8
- export * from "./usePremium";
9
- export * from "./usePremiumGate";
10
- export * from "./useSubscriptionSettingsConfig";
11
- export * from "./useSubscriptionStatus";
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";