@umituz/react-native-subscription 2.22.6 → 2.22.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.22.6",
3
+ "version": "2.22.8",
4
4
  "description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -19,17 +19,34 @@ export type PurchaseSource =
19
19
 
20
20
  export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
21
21
 
22
+ export type SubscriptionStatus = "active" | "expired" | "canceled" | "free";
23
+
24
+ /** Single Source of Truth for user subscription + credits data */
22
25
  export interface UserCredits {
26
+ // Core subscription
27
+ isPremium: boolean;
28
+ status: SubscriptionStatus;
29
+
30
+ // Dates
31
+ purchasedAt: Date | null;
32
+ expirationDate: Date | null;
33
+ lastUpdatedAt: Date | null;
34
+
35
+ // RevenueCat subscription details
36
+ willRenew: boolean;
37
+ productId?: string;
38
+ packageType?: "weekly" | "monthly" | "yearly" | "lifetime";
39
+ originalTransactionId?: string;
40
+
41
+ // Credits
23
42
  credits: number;
24
- packageType?: "weekly" | "monthly" | "yearly";
25
43
  creditLimit?: number;
26
- productId?: string;
44
+
45
+ // Metadata
27
46
  purchaseSource?: PurchaseSource;
28
47
  purchaseType?: PurchaseType;
29
48
  platform?: "ios" | "android";
30
49
  appVersion?: string;
31
- purchasedAt: Date | null;
32
- lastUpdatedAt: Date | null;
33
50
  }
34
51
 
35
52
  export interface CreditAllocation {
@@ -1,27 +1,48 @@
1
- import type { UserCredits } from "../../domain/entities/Credits";
1
+ import type { UserCredits, SubscriptionStatus } from "../../domain/entities/Credits";
2
2
  import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
3
3
 
4
+ /** Maps Firestore document to domain entity */
4
5
  export class CreditsMapper {
5
- static toEntity(snapData: UserCreditsDocumentRead): UserCredits {
6
+ static toEntity(doc: UserCreditsDocumentRead): UserCredits {
7
+ // Determine status from document or derive from isPremium/expirationDate
8
+ const status = doc.status ?? CreditsMapper.deriveStatus(doc);
9
+
6
10
  return {
7
- credits: snapData.credits,
8
- packageType: snapData.packageType,
9
- creditLimit: snapData.creditLimit,
10
- productId: snapData.productId,
11
- purchaseSource: snapData.purchaseSource,
12
- purchaseType: snapData.purchaseType,
13
- platform: snapData.platform,
14
- appVersion: snapData.appVersion,
15
- purchasedAt: snapData.purchasedAt?.toDate?.() || null,
16
- lastUpdatedAt: snapData.lastUpdatedAt?.toDate?.() || null,
11
+ // Core subscription
12
+ isPremium: doc.isPremium ?? false,
13
+ status,
14
+
15
+ // Dates
16
+ purchasedAt: doc.purchasedAt?.toDate?.() ?? null,
17
+ expirationDate: doc.expirationDate?.toDate?.() ?? null,
18
+ lastUpdatedAt: doc.lastUpdatedAt?.toDate?.() ?? null,
19
+
20
+ // RevenueCat details
21
+ willRenew: doc.willRenew ?? false,
22
+ productId: doc.productId,
23
+ packageType: doc.packageType,
24
+ originalTransactionId: doc.originalTransactionId,
25
+
26
+ // Credits
27
+ credits: doc.credits,
28
+ creditLimit: doc.creditLimit,
29
+
30
+ // Metadata
31
+ purchaseSource: doc.purchaseSource,
32
+ purchaseType: doc.purchaseType,
33
+ platform: doc.platform,
34
+ appVersion: doc.appVersion,
17
35
  };
18
36
  }
19
37
 
20
- static toFirestore(data: Partial<UserCredits>): Record<string, any> {
21
- return {
22
- credits: data.credits,
23
- // Timestamps are usually handled by serverTimestamp() in repos,
24
- // but we can map them if needed.
25
- };
38
+ /** Derive status from isPremium and expirationDate for backward compatibility */
39
+ private static deriveStatus(doc: UserCreditsDocumentRead): SubscriptionStatus {
40
+ if (!doc.isPremium && !doc.expirationDate) return "free";
41
+ if (doc.isPremium) return "active";
42
+ if (doc.expirationDate) {
43
+ const expDate = doc.expirationDate.toDate?.();
44
+ if (expDate && expDate < new Date()) return "expired";
45
+ }
46
+ return "free";
26
47
  }
27
48
  }
@@ -1,4 +1,3 @@
1
-
2
1
  export interface FirestoreTimestamp {
3
2
  toDate: () => Date;
4
3
  }
@@ -13,9 +12,11 @@ export type PurchaseSource =
13
12
 
14
13
  export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
15
14
 
15
+ export type SubscriptionDocStatus = "active" | "expired" | "canceled" | "free";
16
+
16
17
  export interface PurchaseMetadata {
17
18
  productId: string;
18
- packageType: "weekly" | "monthly" | "yearly";
19
+ packageType: "weekly" | "monthly" | "yearly" | "lifetime";
19
20
  creditLimit: number;
20
21
  source: PurchaseSource;
21
22
  type: PurchaseType;
@@ -24,19 +25,33 @@ export interface PurchaseMetadata {
24
25
  timestamp: FirestoreTimestamp;
25
26
  }
26
27
 
27
- // Document structure when READING from Firestore
28
+ /** Single Source of Truth for user subscription data */
28
29
  export interface UserCreditsDocumentRead {
30
+ // Core subscription status
31
+ isPremium?: boolean;
32
+ status?: SubscriptionDocStatus;
33
+
34
+ // Dates (all from RevenueCat)
35
+ purchasedAt?: FirestoreTimestamp;
36
+ expirationDate?: FirestoreTimestamp;
37
+ lastUpdatedAt?: FirestoreTimestamp;
38
+ lastPurchaseAt?: FirestoreTimestamp;
39
+
40
+ // RevenueCat subscription details
41
+ willRenew?: boolean;
42
+ productId?: string;
43
+ packageType?: "weekly" | "monthly" | "yearly" | "lifetime";
44
+ originalTransactionId?: string;
45
+
46
+ // Credits
29
47
  credits: number;
30
- packageType?: "weekly" | "monthly" | "yearly";
31
48
  creditLimit?: number;
32
- productId?: string;
49
+
50
+ // Metadata
33
51
  purchaseSource?: PurchaseSource;
34
52
  purchaseType?: PurchaseType;
35
53
  platform?: "ios" | "android";
36
54
  appVersion?: string;
37
- purchasedAt?: FirestoreTimestamp;
38
- lastUpdatedAt?: FirestoreTimestamp;
39
- lastPurchaseAt?: FirestoreTimestamp;
40
55
  processedPurchases?: string[];
41
56
  purchaseHistory?: PurchaseMetadata[];
42
57
  }
@@ -14,12 +14,20 @@ import { getCreditAllocation } from "../../utils/creditMapper";
14
14
 
15
15
  import { CreditsMapper } from "../mappers/CreditsMapper";
16
16
 
17
+ /** RevenueCat subscription data to save (Single Source of Truth) */
18
+ export interface RevenueCatData {
19
+ expirationDate?: string | null;
20
+ willRenew?: boolean;
21
+ originalTransactionId?: string;
22
+ isPremium?: boolean;
23
+ }
24
+
17
25
  export class CreditsRepository extends BaseRepository {
18
26
  constructor(private config: CreditsConfig) { super(); }
19
27
 
20
28
  private getRef(db: Firestore, userId: string) {
21
- return this.config.useUserSubcollection
22
- ? doc(db, "users", userId, "credits", "balance")
29
+ return this.config.useUserSubcollection
30
+ ? doc(db, "users", userId, "credits", "balance")
23
31
  : doc(db, this.config.collectionName, userId);
24
32
  }
25
33
 
@@ -51,7 +59,8 @@ export class CreditsRepository extends BaseRepository {
51
59
  userId: string,
52
60
  purchaseId?: string,
53
61
  productId?: string,
54
- source?: PurchaseSource
62
+ source?: PurchaseSource,
63
+ revenueCatData?: RevenueCatData
55
64
  ): Promise<CreditsResult> {
56
65
  const db = getFirestore();
57
66
  if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
@@ -70,6 +79,11 @@ export class CreditsRepository extends BaseRepository {
70
79
  const metadata: InitializeCreditsMetadata = {
71
80
  productId,
72
81
  source,
82
+ // RevenueCat data for Single Source of Truth
83
+ expirationDate: revenueCatData?.expirationDate,
84
+ willRenew: revenueCatData?.willRenew,
85
+ originalTransactionId: revenueCatData?.originalTransactionId,
86
+ isPremium: revenueCatData?.isPremium,
73
87
  };
74
88
 
75
89
  const res = await initializeCreditsTransaction(
@@ -3,6 +3,7 @@ import Constants from "expo-constants";
3
3
  import {
4
4
  runTransaction,
5
5
  serverTimestamp,
6
+ Timestamp,
6
7
  type Firestore,
7
8
  type FieldValue,
8
9
  type Transaction,
@@ -14,6 +15,7 @@ import type {
14
15
  PurchaseSource,
15
16
  PurchaseType,
16
17
  PurchaseMetadata,
18
+ SubscriptionDocStatus,
17
19
  } from "../models/UserCreditsDocument";
18
20
  import { detectPackageType } from "../../utils/packageTypeDetector";
19
21
  import { getCreditAllocation } from "../../utils/creditMapper";
@@ -22,10 +24,16 @@ interface InitializationResult {
22
24
  credits: number;
23
25
  }
24
26
 
27
+ /** RevenueCat data to save to Firestore (Single Source of Truth) */
25
28
  export interface InitializeCreditsMetadata {
26
29
  productId?: string;
27
30
  source?: PurchaseSource;
28
31
  type?: PurchaseType;
32
+ // RevenueCat subscription data
33
+ expirationDate?: string | null;
34
+ willRenew?: boolean;
35
+ originalTransactionId?: string;
36
+ isPremium?: boolean;
29
37
  }
30
38
 
31
39
  export async function initializeCreditsTransaction(
@@ -114,17 +122,41 @@ export async function initializeCreditsTransaction(
114
122
  ? [...(existing?.purchaseHistory || []), purchaseMetadata].slice(-10)
115
123
  : existing?.purchaseHistory;
116
124
 
117
- // Build credits data, excluding undefined values (Firestore doesn't accept undefined)
125
+ // Determine subscription status
126
+ const isPremium = metadata?.isPremium ?? true;
127
+ const status: SubscriptionDocStatus = isPremium ? "active" : "expired";
128
+
129
+ // Build credits data (Single Source of Truth)
118
130
  const creditsData: Record<string, unknown> = {
131
+ // Core subscription
132
+ isPremium,
133
+ status,
134
+
135
+ // Credits
119
136
  credits: newCredits,
120
137
  creditLimit,
138
+
139
+ // Dates
121
140
  purchasedAt,
122
141
  lastUpdatedAt: now,
123
142
  lastPurchaseAt: now,
143
+
144
+ // Tracking
124
145
  processedPurchases,
125
146
  };
126
147
 
127
- // Only add optional fields if they have values
148
+ // RevenueCat subscription data
149
+ if (metadata?.expirationDate) {
150
+ creditsData.expirationDate = Timestamp.fromDate(new Date(metadata.expirationDate));
151
+ }
152
+ if (metadata?.willRenew !== undefined) {
153
+ creditsData.willRenew = metadata.willRenew;
154
+ }
155
+ if (metadata?.originalTransactionId) {
156
+ creditsData.originalTransactionId = metadata.originalTransactionId;
157
+ }
158
+
159
+ // Package info
128
160
  if (packageType && packageType !== "unknown") {
129
161
  creditsData.packageType = packageType;
130
162
  }
@@ -133,6 +165,8 @@ export async function initializeCreditsTransaction(
133
165
  creditsData.platform = platform;
134
166
  creditsData.appVersion = appVersion;
135
167
  }
168
+
169
+ // Purchase metadata
136
170
  if (metadata?.source) {
137
171
  creditsData.purchaseSource = metadata.source;
138
172
  }
@@ -3,10 +3,12 @@
3
3
  */
4
4
 
5
5
  import { Platform } from "react-native";
6
+ import type { CustomerInfo } from "react-native-purchases";
6
7
  import type { CreditsConfig } from "../../domain/entities/Credits";
7
8
  import { configureCreditsRepository, getCreditsRepository } from "../repositories/CreditsRepositoryProvider";
8
9
  import { SubscriptionManager } from "../../revenuecat/infrastructure/managers/SubscriptionManager";
9
10
  import { configureAuthProvider } from "../../presentation/hooks/useAuthAwarePurchase";
11
+ import type { RevenueCatData } from "../repositories/CreditsRepository";
10
12
 
11
13
  export interface FirebaseAuthLike {
12
14
  currentUser: { uid: string; isAnonymous: boolean } | null;
@@ -61,11 +61,11 @@ export const useSubscriptionSettingsConfig = (
61
61
 
62
62
  // RevenueCat entitlement info - dynamically using configured entitlementId
63
63
  const entitlementId = SubscriptionManager.getEntitlementId() || "premium";
64
- const premiumEntitlement = customerInfo?.entitlements.active[entitlementId];
64
+ const activeEntitlement = customerInfo?.entitlements.active[entitlementId];
65
+ const allEntitlement = customerInfo?.entitlements.all[entitlementId];
65
66
 
66
- // Premium status: use customerInfo directly as it updates in real-time via listener
67
- // This is the source of truth, subscriptionActive is just a backup
68
- const isPremium = !!premiumEntitlement || subscriptionActive;
67
+ // Premium status: only active entitlements count as premium
68
+ const isPremium = !!activeEntitlement || subscriptionActive;
69
69
 
70
70
  const dynamicCreditLimit = useMemo(() => {
71
71
  const config = getCreditsConfig();
@@ -76,8 +76,8 @@ export const useSubscriptionSettingsConfig = (
76
76
  }
77
77
 
78
78
  // 2. FALLBACK: RevenueCat'ten detect et
79
- if (premiumEntitlement?.productIdentifier) {
80
- const packageType = detectPackageType(premiumEntitlement.productIdentifier);
79
+ if (activeEntitlement?.productIdentifier) {
80
+ const packageType = detectPackageType(activeEntitlement.productIdentifier);
81
81
  const allocation = getCreditAllocation(packageType, config.packageAllocations);
82
82
  if (allocation !== null) return allocation;
83
83
  }
@@ -92,19 +92,19 @@ export const useSubscriptionSettingsConfig = (
92
92
 
93
93
  // 4. FINAL FALLBACK: Config'den al
94
94
  return creditLimit ?? config.creditLimit;
95
- }, [credits?.creditLimit, credits?.credits, premiumEntitlement?.productIdentifier, creditLimit]);
96
-
97
- // Get expiration date directly from RevenueCat (source of truth)
98
- const entitlementExpirationDate = premiumEntitlement?.expirationDate ?? null;
99
-
100
- // Prefer CustomerInfo expiration (real-time) over cached status
101
- const expiresAtIso = entitlementExpirationDate || (statusExpirationDate
102
- ? statusExpirationDate.toISOString()
103
- : null);
104
-
105
-
106
-
107
- const willRenew = premiumEntitlement?.willRenew || false;
95
+ }, [credits?.creditLimit, credits?.credits, activeEntitlement?.productIdentifier, creditLimit]);
96
+
97
+ // Get expiration date with fallback chain (supports expired subscriptions)
98
+ // 1. Active entitlement (current subscription)
99
+ // 2. All entitlements (includes expired subscriptions)
100
+ // 3. latestExpirationDate from CustomerInfo
101
+ // 4. Status from Firestore
102
+ const expiresAtIso = activeEntitlement?.expirationDate
103
+ ?? allEntitlement?.expirationDate
104
+ ?? customerInfo?.latestExpirationDate
105
+ ?? (statusExpirationDate ? statusExpirationDate.toISOString() : null);
106
+
107
+ const willRenew = activeEntitlement?.willRenew || false;
108
108
  const purchasedAtIso = convertPurchasedAt(credits?.purchasedAt);
109
109
 
110
110
  // Formatted dates
@@ -129,6 +129,15 @@ export const useSubscriptionSettingsConfig = (
129
129
 
130
130
  const creditsArray = useCreditsArray(credits, dynamicCreditLimit, translations);
131
131
 
132
+ // Centralized display flags - single source of truth for UI visibility
133
+ const hasCredits = creditsArray.length > 0;
134
+ const display = useMemo(() => ({
135
+ showHeader: isPremium || hasCredits,
136
+ showCredits: hasCredits,
137
+ showUpgradePrompt: !isPremium && !hasCredits && !!upgradePrompt,
138
+ showExpirationDate: (isPremium || hasCredits) && !!expiresAtIso,
139
+ }), [isPremium, hasCredits, upgradePrompt, expiresAtIso]);
140
+
132
141
  // Build config
133
142
  const config = useMemo(
134
143
  (): SubscriptionSettingsConfig => ({
@@ -146,6 +155,7 @@ export const useSubscriptionSettingsConfig = (
146
155
  sectionConfig: {
147
156
  statusType,
148
157
  isPremium,
158
+ display,
149
159
  expirationDate: formattedExpirationDate,
150
160
  purchaseDate: formattedPurchaseDate,
151
161
  isLifetime: isPremium && !expiresAtIso,
@@ -175,6 +185,7 @@ export const useSubscriptionSettingsConfig = (
175
185
  translations,
176
186
  isPremium,
177
187
  statusType,
188
+ display,
178
189
  formattedExpirationDate,
179
190
  formattedPurchaseDate,
180
191
  expiresAtIso,
@@ -186,7 +197,5 @@ export const useSubscriptionSettingsConfig = (
186
197
  ]
187
198
  );
188
199
 
189
-
190
-
191
200
  return config;
192
201
  };
@@ -17,6 +17,7 @@ import { DevTestSection } from "./components/DevTestSection";
17
17
  import type { SubscriptionDetailScreenProps } from "../types/SubscriptionDetailTypes";
18
18
 
19
19
  export type {
20
+ SubscriptionDisplayFlags,
20
21
  SubscriptionDetailTranslations,
21
22
  SubscriptionDetailConfig,
22
23
  SubscriptionDetailScreenProps,
@@ -31,8 +32,7 @@ export const SubscriptionDetailScreen: React.FC<
31
32
  SubscriptionDetailScreenProps
32
33
  > = ({ config }) => {
33
34
  const tokens = useAppDesignTokens();
34
- const showCredits = config.credits && config.credits.length > 0;
35
- const showUpgradePrompt = !config.isPremium && config.upgradePrompt && !showCredits;
35
+ const { showHeader, showCredits, showUpgradePrompt, showExpirationDate } = config.display;
36
36
 
37
37
  const styles = useMemo(
38
38
  () =>
@@ -69,10 +69,10 @@ export const SubscriptionDetailScreen: React.FC<
69
69
  }
70
70
  >
71
71
  <View style={styles.cardsContainer}>
72
- {config.isPremium && (
72
+ {showHeader && (
73
73
  <SubscriptionHeader
74
74
  statusType={config.statusType}
75
- isPremium={config.isPremium}
75
+ showExpirationDate={showExpirationDate}
76
76
  isLifetime={config.isLifetime}
77
77
  expirationDate={config.expirationDate}
78
78
  purchaseDate={config.purchaseDate}
@@ -12,7 +12,7 @@ import type { SubscriptionHeaderProps } from "../../types/SubscriptionDetailType
12
12
 
13
13
  export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
14
14
  statusType,
15
- isPremium,
15
+ showExpirationDate,
16
16
  isLifetime,
17
17
  expirationDate,
18
18
  purchaseDate,
@@ -85,41 +85,39 @@ export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
85
85
  />
86
86
  </View>
87
87
 
88
- {isPremium && (
89
- <View style={styles.details}>
90
- {isLifetime ? (
91
- <DetailRow
92
- label={translations.statusLabel}
93
- value={translations.lifetimeLabel}
94
- style={styles.row}
95
- labelStyle={styles.label}
96
- valueStyle={styles.value}
97
- />
98
- ) : (
99
- <>
100
- {expirationDate && (
101
- <DetailRow
102
- label={translations.expiresLabel}
103
- value={expirationDate}
104
- highlight={showExpiring}
105
- style={styles.row}
106
- labelStyle={styles.label}
107
- valueStyle={styles.value}
108
- />
109
- )}
110
- {purchaseDate && (
111
- <DetailRow
112
- label={translations.purchasedLabel}
113
- value={purchaseDate}
114
- style={styles.row}
115
- labelStyle={styles.label}
116
- valueStyle={styles.value}
117
- />
118
- )}
119
- </>
120
- )}
121
- </View>
122
- )}
88
+ <View style={styles.details}>
89
+ {isLifetime ? (
90
+ <DetailRow
91
+ label={translations.statusLabel}
92
+ value={translations.lifetimeLabel}
93
+ style={styles.row}
94
+ labelStyle={styles.label}
95
+ valueStyle={styles.value}
96
+ />
97
+ ) : (
98
+ <>
99
+ {showExpirationDate && expirationDate && (
100
+ <DetailRow
101
+ label={translations.expiresLabel}
102
+ value={expirationDate}
103
+ highlight={showExpiring}
104
+ style={styles.row}
105
+ labelStyle={styles.label}
106
+ valueStyle={styles.value}
107
+ />
108
+ )}
109
+ {purchaseDate && (
110
+ <DetailRow
111
+ label={translations.purchasedLabel}
112
+ value={purchaseDate}
113
+ style={styles.row}
114
+ labelStyle={styles.label}
115
+ valueStyle={styles.value}
116
+ />
117
+ )}
118
+ </>
119
+ )}
120
+ </View>
123
121
  </View>
124
122
  );
125
123
  };
@@ -53,10 +53,19 @@ export interface UpgradePromptConfig {
53
53
  benefits?: UpgradeBenefit[];
54
54
  }
55
55
 
56
+ /** Display flags - centralized UI visibility control */
57
+ export interface SubscriptionDisplayFlags {
58
+ showHeader: boolean;
59
+ showCredits: boolean;
60
+ showUpgradePrompt: boolean;
61
+ showExpirationDate: boolean;
62
+ }
63
+
56
64
  /** Configuration for subscription detail screen */
57
65
  export interface SubscriptionDetailConfig {
58
66
  statusType: SubscriptionStatusType;
59
67
  isPremium: boolean;
68
+ display: SubscriptionDisplayFlags;
60
69
  expirationDate?: string | null;
61
70
  purchaseDate?: string | null;
62
71
  isLifetime?: boolean;
@@ -78,7 +87,7 @@ export interface SubscriptionDetailScreenProps {
78
87
  /** Props for subscription header component */
79
88
  export interface SubscriptionHeaderProps {
80
89
  statusType: SubscriptionStatusType;
81
- isPremium: boolean;
90
+ showExpirationDate: boolean;
82
91
  isLifetime?: boolean;
83
92
  expirationDate?: string | null;
84
93
  purchaseDate?: string | null;