@umituz/react-native-subscription 2.27.123 → 2.27.125

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/creditOperationUtils.ts +65 -144
  3. package/src/domains/credits/application/creditOperationUtils.types.ts +19 -0
  4. package/src/domains/credits/presentation/useCredits.ts +1 -11
  5. package/src/domains/paywall/components/PaywallContainer.tsx +3 -1
  6. package/src/domains/paywall/components/PaywallModal.tsx +19 -107
  7. package/src/domains/paywall/components/PaywallModal.types.ts +26 -0
  8. package/src/domains/paywall/components/PlanCard.tsx +45 -148
  9. package/src/domains/paywall/components/PlanCard.types.ts +12 -0
  10. package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +116 -0
  11. package/src/domains/subscription/application/SubscriptionSyncService.ts +17 -99
  12. package/src/domains/subscription/application/SubscriptionSyncUtils.ts +0 -2
  13. package/src/domains/subscription/core/SubscriptionConstants.ts +1 -13
  14. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +7 -13
  15. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.types.ts +15 -0
  16. package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -1
  17. package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +20 -5
  18. package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +13 -92
  19. package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.types.ts +47 -0
  20. package/src/domains/subscription/presentation/screens/components/UpgradePrompt.tsx +34 -126
  21. package/src/domains/subscription/presentation/screens/components/UpgradePrompt.types.ts +12 -0
  22. package/src/domains/subscription/presentation/usePremium.ts +3 -22
  23. package/src/domains/subscription/presentation/usePremium.types.ts +16 -0
  24. package/src/domains/subscription/presentation/useSubscriptionStatus.ts +30 -22
  25. package/src/domains/subscription/presentation/useSubscriptionStatus.types.ts +7 -0
  26. package/src/domains/wallet/index.ts +6 -2
  27. package/src/domains/wallet/infrastructure/config/walletConfig.ts +2 -1
  28. package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +3 -16
  29. package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +1 -13
  30. package/src/domains/wallet/presentation/screens/WalletScreen.tsx +25 -112
  31. package/src/domains/wallet/presentation/screens/WalletScreen.types.ts +15 -0
  32. package/src/shared/utils/appValidators.ts +38 -0
  33. package/src/shared/utils/validators.ts +4 -122
  34. package/src/domains/paywall/components/README.md +0 -41
  35. package/src/domains/subscription/presentation/screens/README.md +0 -52
@@ -1,160 +1,57 @@
1
- /**
2
- * Plan Card
3
- * Subscription plan selection card (Apple-compliant)
4
- *
5
- * Apple Guideline 3.1.2 Compliance:
6
- * - Price is the most prominent element
7
- * - Trial info is displayed in subordinate position and size
8
- * - No toggle for enabling/disabling trial
9
- */
10
-
11
1
  import React from "react";
12
2
  import { View, TouchableOpacity, StyleSheet } from "react-native";
13
3
  import { AtomicText, AtomicIcon, AtomicBadge, useAppDesignTokens } from "@umituz/react-native-design-system";
14
- import type { PurchasesPackage } from "react-native-purchases";
15
-
16
4
  import { formatPriceWithPeriod } from '../../../utils/priceUtils';
17
-
18
- interface PlanCardProps {
19
- pkg: PurchasesPackage;
20
- isSelected: boolean;
21
- onSelect: () => void;
22
- /** Badge text (e.g., "Best Value") - NOT for trial */
23
- badge?: string;
24
- /** Credit amount for this plan */
25
- creditAmount?: number;
26
- /** Credits label text (e.g., "credits") */
27
- creditsLabel?: string;
28
- /** Whether this plan has a free trial (Apple-compliant display) */
29
- hasFreeTrial?: boolean;
30
- /** Trial subtitle text (e.g., "7 days free, then billed") - shown as small gray text */
31
- trialSubtitleText?: string;
32
- }
5
+ import { PlanCardProps } from "./PlanCard.types";
33
6
 
34
7
  export const PlanCard: React.FC<PlanCardProps> = React.memo(
35
- ({ pkg, isSelected, onSelect, badge, creditAmount, creditsLabel, hasFreeTrial, trialSubtitleText }) => {
36
- const tokens = useAppDesignTokens();
37
- const title = pkg.product.title;
38
- const price = formatPriceWithPeriod(pkg.product.price, pkg.product.currencyCode, pkg.identifier);
39
-
40
- return (
41
- <TouchableOpacity onPress={onSelect} activeOpacity={0.7} style={styles.touchable}>
42
- <View
43
- style={[
44
- styles.container,
45
- {
46
- backgroundColor: tokens.colors.surface,
47
- borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
48
- borderWidth: isSelected ? 2 : 1,
49
- },
50
- ]}
51
- >
52
- {/* Badge for "Best Value" etc. - NOT for trial (Apple compliance) */}
53
- {badge && (
54
- <View style={styles.badgeContainer}>
55
- <AtomicBadge text={badge} variant="primary" size="sm" />
56
- </View>
57
- )}
58
-
59
- <View style={styles.content}>
60
- <View style={styles.leftSection}>
61
- <View
62
- style={[
63
- styles.radio,
64
- {
65
- borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
66
- backgroundColor: isSelected ? tokens.colors.primary : "transparent",
67
- },
68
- ]}
69
- >
70
- {isSelected && (
71
- <AtomicIcon name="checkmark-circle-outline" customSize={12} customColor={tokens.colors.onPrimary} />
72
- )}
73
- </View>
74
-
75
- <View style={styles.textSection}>
76
- <AtomicText type="titleSmall" style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}>
77
- {title}
78
- </AtomicText>
79
-
80
- {/* Credits info */}
81
- {creditAmount != null && creditAmount > 0 && creditsLabel && (
82
- <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
83
- {creditAmount} {creditsLabel}
84
- </AtomicText>
85
- )}
86
-
87
- {/* Trial info - Apple-compliant: small, gray, subordinate */}
88
- {hasFreeTrial && trialSubtitleText && (
89
- <AtomicText
90
- type="bodySmall"
91
- style={{
92
- color: tokens.colors.textTertiary ?? tokens.colors.textSecondary,
93
- fontSize: 11,
94
- marginTop: 2,
95
- }}
96
- >
97
- {trialSubtitleText}
98
- </AtomicText>
99
- )}
100
- </View>
101
- </View>
102
-
103
- {/* Price - MOST PROMINENT (Apple compliance) */}
104
- <AtomicText
105
- type="titleMedium"
106
- style={{
107
- color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary,
108
- fontWeight: "700",
109
- fontSize: 18,
110
- }}
111
- >
112
- {price}
113
- </AtomicText>
114
- </View>
115
- </View>
116
- </TouchableOpacity>
117
- );
118
- }
8
+ ({ pkg, isSelected, onSelect, badge, creditAmount, creditsLabel, hasFreeTrial, trialSubtitleText }) => {
9
+ const tokens = useAppDesignTokens();
10
+ const title = pkg.product.title;
11
+ const price = formatPriceWithPeriod(pkg.product.price, pkg.product.currencyCode, pkg.identifier);
12
+
13
+ return (
14
+ <TouchableOpacity onPress={onSelect} activeOpacity={0.7} style={styles.touchable}>
15
+ <View style={[styles.container, {
16
+ backgroundColor: tokens.colors.surface,
17
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
18
+ borderWidth: isSelected ? 2 : 1
19
+ }]}>
20
+ {badge && <View style={styles.badgeContainer}><AtomicBadge text={badge} variant="primary" size="sm" /></View>}
21
+ <View style={styles.content}>
22
+ <View style={styles.leftSection}>
23
+ <View style={[styles.radio, {
24
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
25
+ backgroundColor: isSelected ? tokens.colors.primary : "transparent"
26
+ }]}>
27
+ {isSelected && <AtomicIcon name="checkmark-circle-outline" customSize={12} customColor={tokens.colors.onPrimary} />}
28
+ </View>
29
+ <View style={styles.textSection}>
30
+ <AtomicText type="titleSmall" style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}>{title}</AtomicText>
31
+ {creditAmount != null && creditAmount > 0 && creditsLabel && (
32
+ <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>{creditAmount} {creditsLabel}</AtomicText>
33
+ )}
34
+ {hasFreeTrial && trialSubtitleText && (
35
+ <AtomicText type="bodySmall" style={{ color: tokens.colors.textTertiary ?? tokens.colors.textSecondary, fontSize: 11, marginTop: 2 }}>{trialSubtitleText}</AtomicText>
36
+ )}
37
+ </View>
38
+ </View>
39
+ <AtomicText type="titleMedium" style={{ color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary, fontWeight: "700", fontSize: 18 }}>{price}</AtomicText>
40
+ </View>
41
+ </View>
42
+ </TouchableOpacity>
43
+ );
44
+ }
119
45
  );
120
46
 
121
47
  PlanCard.displayName = "PlanCard";
122
48
 
123
49
  const styles = StyleSheet.create({
124
- touchable: {
125
- marginBottom: 10,
126
- marginHorizontal: 24,
127
- },
128
- container: {
129
- borderRadius: 16,
130
- padding: 16,
131
- position: "relative",
132
- },
133
- badgeContainer: {
134
- position: "absolute",
135
- top: -10,
136
- right: 16,
137
- },
138
- content: {
139
- flexDirection: "row",
140
- alignItems: "center",
141
- justifyContent: "space-between",
142
- },
143
- leftSection: {
144
- flexDirection: "row",
145
- alignItems: "center",
146
- flex: 1,
147
- },
148
- radio: {
149
- width: 22,
150
- height: 22,
151
- borderRadius: 11,
152
- borderWidth: 2,
153
- alignItems: "center",
154
- justifyContent: "center",
155
- marginRight: 12,
156
- },
157
- textSection: {
158
- flex: 1,
159
- },
50
+ touchable: { marginBottom: 10, marginHorizontal: 24 },
51
+ container: { borderRadius: 16, padding: 16, position: "relative" },
52
+ badgeContainer: { position: "absolute", top: -10, right: 16 },
53
+ content: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" },
54
+ leftSection: { flexDirection: "row", alignItems: "center", flex: 1 },
55
+ radio: { width: 22, height: 22, borderRadius: 11, borderWidth: 2, alignItems: "center", justifyContent: "center", marginRight: 12 },
56
+ textSection: { flex: 1 },
160
57
  });
@@ -0,0 +1,12 @@
1
+ import type { PurchasesPackage } from "react-native-purchases";
2
+
3
+ export interface PlanCardProps {
4
+ pkg: PurchasesPackage;
5
+ isSelected: boolean;
6
+ onSelect: () => void;
7
+ badge?: string;
8
+ creditAmount?: number;
9
+ creditsLabel?: string;
10
+ hasFreeTrial?: boolean;
11
+ trialSubtitleText?: string;
12
+ }
@@ -0,0 +1,116 @@
1
+ import type { CustomerInfo } from "react-native-purchases";
2
+ import type { RevenueCatData } from "../core/RevenueCatData";
3
+ import {
4
+ type PeriodType,
5
+ type PurchaseSource,
6
+ PURCHASE_SOURCE,
7
+ PURCHASE_TYPE
8
+ } from "../core/SubscriptionConstants";
9
+ import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
10
+ import { extractRevenueCatData } from "./SubscriptionSyncUtils";
11
+ import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
12
+
13
+ export class SubscriptionSyncProcessor {
14
+ constructor(private entitlementId: string) {}
15
+
16
+ async processPurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource) {
17
+ const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
18
+ const purchaseId = revenueCatData.originalTransactionId
19
+ ? `purchase_${revenueCatData.originalTransactionId}`
20
+ : `purchase_${productId}_${Date.now()}`;
21
+
22
+ await getCreditsRepository().initializeCredits(
23
+ userId,
24
+ purchaseId,
25
+ productId,
26
+ source ?? PURCHASE_SOURCE.SETTINGS,
27
+ revenueCatData,
28
+ PURCHASE_TYPE.INITIAL
29
+ );
30
+
31
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
32
+ }
33
+
34
+ async processRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
35
+ const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
36
+ revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
37
+ const purchaseId = revenueCatData.originalTransactionId
38
+ ? `renewal_${revenueCatData.originalTransactionId}_${newExpirationDate}`
39
+ : `renewal_${productId}_${Date.now()}`;
40
+
41
+ await getCreditsRepository().initializeCredits(
42
+ userId,
43
+ purchaseId,
44
+ productId,
45
+ PURCHASE_SOURCE.RENEWAL,
46
+ revenueCatData,
47
+ PURCHASE_TYPE.RENEWAL
48
+ );
49
+
50
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
51
+ }
52
+
53
+ async processStatusChange(
54
+ userId: string,
55
+ isPremium: boolean,
56
+ productId?: string,
57
+ expiresAt?: string,
58
+ willRenew?: boolean,
59
+ periodType?: PeriodType
60
+ ) {
61
+ const repository = getCreditsRepository();
62
+
63
+ if (!isPremium && !productId) {
64
+ const currentCredits = await repository.getCredits(userId);
65
+ if (currentCredits.success && currentCredits.data?.isPremium) {
66
+ return;
67
+ }
68
+ }
69
+
70
+ if (!isPremium && productId) {
71
+ await repository.syncExpiredStatus(userId);
72
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
73
+ return;
74
+ }
75
+
76
+ if (!isPremium && !productId) {
77
+ const stableSyncId = `init_sync_${userId}`;
78
+ await repository.initializeCredits(
79
+ userId,
80
+ stableSyncId,
81
+ 'no_subscription',
82
+ PURCHASE_SOURCE.SETTINGS,
83
+ {
84
+ isPremium: false,
85
+ expirationDate: null,
86
+ willRenew: false,
87
+ periodType: null,
88
+ originalTransactionId: null
89
+ },
90
+ PURCHASE_TYPE.INITIAL
91
+ );
92
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
93
+ return;
94
+ }
95
+
96
+ const revenueCatData: RevenueCatData = {
97
+ expirationDate: expiresAt ?? null,
98
+ willRenew: willRenew ?? false,
99
+ isPremium,
100
+ periodType: periodType ?? null,
101
+ originalTransactionId: null
102
+ };
103
+
104
+ const statusSyncId = `status_sync_${userId}_${isPremium ? 'premium' : 'free'}`;
105
+ await repository.initializeCredits(
106
+ userId,
107
+ statusSyncId,
108
+ productId ?? 'no_subscription',
109
+ PURCHASE_SOURCE.SETTINGS,
110
+ revenueCatData,
111
+ PURCHASE_TYPE.INITIAL
112
+ );
113
+
114
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
115
+ }
116
+ }
@@ -1,61 +1,30 @@
1
1
  import type { CustomerInfo } from "react-native-purchases";
2
- import type { RevenueCatData } from "../core/RevenueCatData";
3
- import { type PeriodType, type PurchaseSource, PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
4
- import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
5
- import { extractRevenueCatData } from "./SubscriptionSyncUtils";
2
+ import { type PeriodType, type PurchaseSource } from "../core/SubscriptionConstants";
6
3
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
4
+ import { SubscriptionSyncProcessor } from "./SubscriptionSyncProcessor";
7
5
 
8
- /**
9
- * Service to synchronize RevenueCat state with Firestore.
10
- * Acts as a subscriber/handler for subscription events.
11
- */
12
6
  export class SubscriptionSyncService {
13
- constructor(private entitlementId: string) {}
7
+ private processor: SubscriptionSyncProcessor;
8
+
9
+ constructor(entitlementId: string) {
10
+ this.processor = new SubscriptionSyncProcessor(entitlementId);
11
+ }
14
12
 
15
13
  async handlePurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource) {
16
14
  try {
17
- const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
18
- const purchaseId = revenueCatData.originalTransactionId
19
- ? `purchase_${revenueCatData.originalTransactionId}`
20
- : `purchase_${productId}_${Date.now()}`;
21
-
22
- await getCreditsRepository().initializeCredits(
23
- userId,
24
- purchaseId,
25
- productId,
26
- source ?? PURCHASE_SOURCE.SETTINGS,
27
- revenueCatData,
28
- PURCHASE_TYPE.INITIAL
29
- );
30
-
31
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
15
+ await this.processor.processPurchase(userId, productId, customerInfo, source);
32
16
  subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, { userId, productId });
33
- } catch {
34
- // Ignored to prevent background sync errors from disrupting user experience
17
+ } catch (err) {
18
+ console.error("[SubscriptionSyncService] purchase error:", err);
35
19
  }
36
20
  }
37
21
 
38
22
  async handleRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
39
23
  try {
40
- const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
41
- revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
42
- const purchaseId = revenueCatData.originalTransactionId
43
- ? `renewal_${revenueCatData.originalTransactionId}_${newExpirationDate}`
44
- : `renewal_${productId}_${Date.now()}`;
45
-
46
- await getCreditsRepository().initializeCredits(
47
- userId,
48
- purchaseId,
49
- productId,
50
- PURCHASE_SOURCE.RENEWAL,
51
- revenueCatData,
52
- PURCHASE_TYPE.RENEWAL
53
- );
54
-
55
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
24
+ await this.processor.processRenewal(userId, productId, newExpirationDate, customerInfo);
56
25
  subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.RENEWAL_DETECTED, { userId, productId });
57
- } catch {
58
- // Ignored to prevent background sync errors from disrupting user experience
26
+ } catch (err) {
27
+ console.error("[SubscriptionSyncService] renewal error:", err);
59
28
  }
60
29
  }
61
30
 
@@ -68,62 +37,11 @@ export class SubscriptionSyncService {
68
37
  periodType?: PeriodType
69
38
  ) {
70
39
  try {
71
- // Handle subscription expiration explicitly
72
- if (!isPremium && productId) {
73
- await getCreditsRepository().syncExpiredStatus(userId);
74
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
75
- return;
76
- }
77
-
78
- // If not premium and no product, this is a freemium user.
79
- // We only want to run initializeCredits for them if it's their first time,
80
- // which initializeCredits handles, but we should avoid doing it on every sync.
81
- if (!isPremium && !productId) {
82
- // Option 1: Just skip if they are already known non-premium (handled by repository check)
83
- // For now, let's just use a more stable sync ID to allow the repository to skip if possible
84
- const stableSyncId = `init_sync_${userId}`;
85
-
86
- await getCreditsRepository().initializeCredits(
87
- userId,
88
- stableSyncId,
89
- 'no_subscription',
90
- PURCHASE_SOURCE.SETTINGS,
91
- {
92
- isPremium: false,
93
- expirationDate: null,
94
- willRenew: false,
95
- periodType: null,
96
- originalTransactionId: null
97
- },
98
- PURCHASE_TYPE.INITIAL
99
- );
100
-
101
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
102
- return;
103
- }
104
-
105
- // Standard status sync for premium users
106
- const revenueCatData: RevenueCatData = {
107
- expirationDate: expiresAt ?? null,
108
- willRenew: willRenew ?? false,
109
- isPremium,
110
- periodType: periodType ?? null,
111
- originalTransactionId: null
112
- };
113
-
114
- await getCreditsRepository().initializeCredits(
115
- userId,
116
- `status_sync_${Date.now()}`,
117
- productId ?? 'no_subscription',
118
- PURCHASE_SOURCE.SETTINGS,
119
- revenueCatData,
120
- PURCHASE_TYPE.INITIAL
121
- );
122
-
123
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
40
+ await this.processor.processStatusChange(userId, isPremium, productId, expiresAt, willRenew, periodType);
124
41
  subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED, { userId, isPremium });
125
- } catch {
126
- // Ignored to prevent background sync errors from disrupting user experience
42
+ } catch (err) {
43
+ console.error("[SubscriptionSyncService] status change error:", err);
127
44
  }
128
45
  }
129
46
  }
47
+
@@ -2,7 +2,6 @@ import type { CustomerInfo } from "react-native-purchases";
2
2
  import type { RevenueCatData } from "../core/RevenueCatData";
3
3
  import { type PeriodType } from "../core/SubscriptionStatus";
4
4
 
5
- /** Extract RevenueCat data from CustomerInfo (Single Source of Truth) */
6
5
  export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId: string): RevenueCatData => {
7
6
  const entitlement = customerInfo.entitlements.active[entitlementId]
8
7
  ?? customerInfo.entitlements.all[entitlementId];
@@ -10,7 +9,6 @@ export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId:
10
9
  return {
11
10
  expirationDate: entitlement?.expirationDate ?? customerInfo.latestExpirationDate ?? null,
12
11
  willRenew: entitlement?.willRenew ?? false,
13
- // Use latestPurchaseDate if originalPurchaseDate is missing, or a combine id
14
12
  originalTransactionId: entitlement?.originalPurchaseDate || customerInfo.firstSeen,
15
13
  periodType: (entitlement?.periodType as PeriodType) ?? null,
16
14
  isPremium: !!customerInfo.entitlements.active[entitlementId],
@@ -1,9 +1,3 @@
1
- /**
2
- * Subscription Constants and Types
3
- * Centralized source of truth for subscription-related enums and types.
4
- */
5
-
6
- /** User tier constants */
7
1
  export const USER_TIER = {
8
2
  ANONYMOUS: 'anonymous',
9
3
  FREEMIUM: 'freemium',
@@ -12,10 +6,8 @@ export const USER_TIER = {
12
6
 
13
7
  export type UserTierType = (typeof USER_TIER)[keyof typeof USER_TIER];
14
8
 
15
- /** Default entitlement identifier */
16
9
  export const DEFAULT_ENTITLEMENT_ID = 'premium';
17
10
 
18
- /** Subscription status constants */
19
11
  export const SUBSCRIPTION_STATUS = {
20
12
  ACTIVE: 'active',
21
13
  TRIAL: 'trial',
@@ -27,7 +19,6 @@ export const SUBSCRIPTION_STATUS = {
27
19
 
28
20
  export type SubscriptionStatusType = (typeof SUBSCRIPTION_STATUS)[keyof typeof SUBSCRIPTION_STATUS];
29
21
 
30
- /** RevenueCat period type constants */
31
22
  export const PERIOD_TYPE = {
32
23
  NORMAL: 'NORMAL',
33
24
  INTRO: 'INTRO',
@@ -36,7 +27,6 @@ export const PERIOD_TYPE = {
36
27
 
37
28
  export type PeriodType = (typeof PERIOD_TYPE)[keyof typeof PERIOD_TYPE];
38
29
 
39
- /** Subscription package type constants */
40
30
  export const PACKAGE_TYPE = {
41
31
  WEEKLY: 'weekly',
42
32
  MONTHLY: 'monthly',
@@ -47,7 +37,6 @@ export const PACKAGE_TYPE = {
47
37
 
48
38
  export type PackageType = (typeof PACKAGE_TYPE)[keyof typeof PACKAGE_TYPE];
49
39
 
50
- /** Platform constants */
51
40
  export const PLATFORM = {
52
41
  IOS: 'ios',
53
42
  ANDROID: 'android',
@@ -55,7 +44,6 @@ export const PLATFORM = {
55
44
 
56
45
  export type Platform = (typeof PLATFORM)[keyof typeof PLATFORM];
57
46
 
58
- /** Purchase source constants */
59
47
  export const PURCHASE_SOURCE = {
60
48
  ONBOARDING: 'onboarding',
61
49
  SETTINGS: 'settings',
@@ -68,7 +56,6 @@ export const PURCHASE_SOURCE = {
68
56
 
69
57
  export type PurchaseSource = (typeof PURCHASE_SOURCE)[keyof typeof PURCHASE_SOURCE];
70
58
 
71
- /** Purchase type constants */
72
59
  export const PURCHASE_TYPE = {
73
60
  INITIAL: 'initial',
74
61
  RENEWAL: 'renewal',
@@ -77,3 +64,4 @@ export const PURCHASE_TYPE = {
77
64
  } as const;
78
65
 
79
66
  export type PurchaseType = (typeof PURCHASE_TYPE)[keyof typeof PURCHASE_TYPE];
67
+
@@ -1,14 +1,7 @@
1
- /**
2
- * Subscription Manager
3
- * Facade for subscription operations. Coordinates state and operations.
4
- */
5
-
6
1
  import type { PurchasesPackage } from "react-native-purchases";
7
- import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
8
2
  import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
9
3
  import { initializeRevenueCatService, getRevenueCatService } from "../services/RevenueCatService";
10
4
  import { PackageHandler } from "../handlers/PackageHandler";
11
- import type { PremiumStatus, RestoreResultInfo } from "../handlers/PackageHandler";
12
5
  import { SubscriptionInternalState } from "./SubscriptionInternalState";
13
6
  import {
14
7
  ensureConfigured,
@@ -17,11 +10,12 @@ import {
17
10
  ensureServiceAvailable,
18
11
  } from "./subscriptionManagerUtils";
19
12
 
20
- export interface SubscriptionManagerConfig {
21
- config: RevenueCatConfig;
22
- apiKey: string;
23
- getAnonymousUserId: () => Promise<string>;
24
- }
13
+ import type {
14
+ SubscriptionManagerConfig,
15
+ PremiumStatus,
16
+ RestoreResultInfo
17
+ } from "./SubscriptionManager.types";
18
+
25
19
 
26
20
  class SubscriptionManagerImpl {
27
21
  private managerConfig: SubscriptionManagerConfig | null = null;
@@ -142,4 +136,4 @@ class SubscriptionManagerImpl {
142
136
  }
143
137
 
144
138
  export const SubscriptionManager = new SubscriptionManagerImpl();
145
- export type { PremiumStatus };
139
+
@@ -0,0 +1,15 @@
1
+ import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
2
+ import type { PremiumStatus } from "../handlers/PurchaseStatusResolver";
3
+
4
+ export interface SubscriptionManagerConfig {
5
+ config: RevenueCatConfig;
6
+ apiKey: string;
7
+ getAnonymousUserId: () => Promise<string>;
8
+ }
9
+
10
+ export type { PremiumStatus };
11
+
12
+ export interface RestoreResultInfo {
13
+ success: boolean;
14
+ productId: string | null;
15
+ }
@@ -3,7 +3,8 @@
3
3
  * Validation and helper functions for SubscriptionManager
4
4
  */
5
5
 
6
- import type { SubscriptionManagerConfig } from "./SubscriptionManager";
6
+ import type { SubscriptionManagerConfig } from "./SubscriptionManager.types";
7
+
7
8
  import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
8
9
  import { SubscriptionInternalState } from "./SubscriptionInternalState";
9
10