@umituz/react-native-subscription 2.22.2 → 2.22.4

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.2",
3
+ "version": "2.22.4",
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",
@@ -61,8 +61,30 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
61
61
  }
62
62
  };
63
63
 
64
+ const onRenewal = async (userId: string, productId: string, _newExpirationDate: string, _customerInfo: unknown) => {
65
+ if (__DEV__) {
66
+ console.log('[SubscriptionInitializer] onRenewal called:', { userId, productId });
67
+ }
68
+ try {
69
+ const result = await getCreditsRepository().initializeCredits(
70
+ userId,
71
+ `renewal_${productId}_${Date.now()}`,
72
+ productId,
73
+ "renewal" as any
74
+ );
75
+ if (__DEV__) {
76
+ console.log('[SubscriptionInitializer] Credits reset on renewal:', result);
77
+ }
78
+ onCreditsUpdated?.(userId);
79
+ } catch (error) {
80
+ if (__DEV__) {
81
+ console.error('[SubscriptionInitializer] Renewal credits init failed:', error);
82
+ }
83
+ }
84
+ };
85
+
64
86
  SubscriptionManager.configure({
65
- config: { apiKey: key, testStoreKey, entitlementIdentifier: entitlementId, consumableProductIdentifiers: [creditPackages?.identifierPattern || "credit"], onPurchaseCompleted: onPurchase, onCreditsUpdated },
87
+ config: { apiKey: key, testStoreKey, entitlementIdentifier: entitlementId, consumableProductIdentifiers: [creditPackages?.identifierPattern || "credit"], onPurchaseCompleted: onPurchase, onRenewalDetected: onRenewal, onCreditsUpdated },
66
88
  apiKey: key, getAnonymousUserId
67
89
  });
68
90
 
@@ -31,7 +31,7 @@ export const SubscriptionDetailScreen: React.FC<
31
31
  SubscriptionDetailScreenProps
32
32
  > = ({ config }) => {
33
33
  const tokens = useAppDesignTokens();
34
- const showCredits = config.isPremium && config.credits && config.credits.length > 0;
34
+ const showCredits = config.credits && config.credits.length > 0;
35
35
  const showUpgradePrompt = !config.isPremium && config.upgradePrompt;
36
36
 
37
37
  const styles = useMemo(
@@ -68,8 +68,8 @@ export const SubscriptionDetailScreen: React.FC<
68
68
  ) : undefined
69
69
  }
70
70
  >
71
- {config.isPremium ? (
72
- <View style={styles.cardsContainer}>
71
+ <View style={styles.cardsContainer}>
72
+ {config.isPremium && (
73
73
  <SubscriptionHeader
74
74
  statusType={config.statusType}
75
75
  isPremium={config.isPremium}
@@ -79,20 +79,20 @@ export const SubscriptionDetailScreen: React.FC<
79
79
  daysRemaining={config.daysRemaining}
80
80
  translations={config.translations}
81
81
  />
82
+ )}
82
83
 
83
- {showCredits && (
84
- <CreditsList
85
- credits={config.credits!}
86
- title={
87
- config.translations.usageTitle || config.translations.creditsTitle
88
- }
89
- description={config.translations.creditsResetInfo}
90
- remainingLabel={config.translations.remainingLabel}
91
- />
92
- )}
93
- </View>
94
- ) : (
95
- showUpgradePrompt && (
84
+ {showCredits && (
85
+ <CreditsList
86
+ credits={config.credits!}
87
+ title={
88
+ config.translations.usageTitle || config.translations.creditsTitle
89
+ }
90
+ description={config.translations.creditsResetInfo}
91
+ remainingLabel={config.translations.remainingLabel}
92
+ />
93
+ )}
94
+
95
+ {showUpgradePrompt && (
96
96
  <UpgradePrompt
97
97
  title={config.upgradePrompt!.title}
98
98
  subtitle={config.upgradePrompt!.subtitle}
@@ -100,8 +100,8 @@ export const SubscriptionDetailScreen: React.FC<
100
100
  upgradeButtonLabel={config.translations.upgradeButton}
101
101
  onUpgrade={config.onUpgrade}
102
102
  />
103
- )
104
- )}
103
+ )}
104
+ </View>
105
105
 
106
106
  <View style={styles.spacer} />
107
107
  </ScreenLayout>
@@ -34,6 +34,13 @@ export interface RevenueCatConfig {
34
34
  isPremium: boolean,
35
35
  customerInfo: CustomerInfo
36
36
  ) => Promise<void> | void;
37
+ /** Callback when subscription renewal is detected */
38
+ onRenewalDetected?: (
39
+ userId: string,
40
+ productId: string,
41
+ newExpirationDate: string,
42
+ customerInfo: CustomerInfo
43
+ ) => Promise<void> | void;
37
44
  /** Callback after credits are successfully updated (for cache invalidation) */
38
45
  onCreditsUpdated?: (userId: string) => void;
39
46
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Customer Info Listener Manager
3
- * Handles RevenueCat customer info update listeners
3
+ * Handles RevenueCat customer info update listeners with renewal detection
4
4
  */
5
5
 
6
6
  import Purchases, {
@@ -9,10 +9,21 @@ import Purchases, {
9
9
  } from "react-native-purchases";
10
10
  import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
11
11
  import { syncPremiumStatus } from "../utils/PremiumStatusSyncer";
12
+ import {
13
+ detectRenewal,
14
+ updateRenewalState,
15
+ type RenewalState,
16
+ } from "../utils/RenewalDetector";
17
+
18
+ declare const __DEV__: boolean;
12
19
 
13
20
  export class CustomerInfoListenerManager {
14
21
  private listener: CustomerInfoUpdateListener | null = null;
15
22
  private currentUserId: string | null = null;
23
+ private renewalState: RenewalState = {
24
+ previousExpirationDate: null,
25
+ previousProductId: null,
26
+ };
16
27
 
17
28
  setUserId(userId: string): void {
18
29
  this.currentUserId = userId;
@@ -20,6 +31,10 @@ export class CustomerInfoListenerManager {
20
31
 
21
32
  clearUserId(): void {
22
33
  this.currentUserId = null;
34
+ this.renewalState = {
35
+ previousExpirationDate: null,
36
+ previousProductId: null,
37
+ };
23
38
  }
24
39
 
25
40
  setupListener(config: RevenueCatConfig): void {
@@ -30,6 +45,37 @@ export class CustomerInfoListenerManager {
30
45
  return;
31
46
  }
32
47
 
48
+ const renewalResult = detectRenewal(
49
+ this.renewalState,
50
+ customerInfo,
51
+ config.entitlementIdentifier
52
+ );
53
+
54
+ if (renewalResult.isRenewal && config.onRenewalDetected) {
55
+ if (__DEV__) {
56
+ console.log("[CustomerInfoListener] Renewal detected:", {
57
+ userId: this.currentUserId,
58
+ productId: renewalResult.productId,
59
+ newExpiration: renewalResult.newExpirationDate,
60
+ });
61
+ }
62
+
63
+ try {
64
+ await config.onRenewalDetected(
65
+ this.currentUserId,
66
+ renewalResult.productId!,
67
+ renewalResult.newExpirationDate!,
68
+ customerInfo
69
+ );
70
+ } catch (error) {
71
+ if (__DEV__) {
72
+ console.error("[CustomerInfoListener] Renewal callback failed:", error);
73
+ }
74
+ }
75
+ }
76
+
77
+ this.renewalState = updateRenewalState(this.renewalState, renewalResult);
78
+
33
79
  syncPremiumStatus(config, this.currentUserId, customerInfo);
34
80
  };
35
81
 
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Renewal Detector
3
+ * Detects subscription renewals by tracking expiration date changes
4
+ * Best Practice: Compare expiration dates to detect renewal events
5
+ */
6
+
7
+ import type { CustomerInfo } from "react-native-purchases";
8
+
9
+ export interface RenewalState {
10
+ previousExpirationDate: string | null;
11
+ previousProductId: string | null;
12
+ }
13
+
14
+ export interface RenewalDetectionResult {
15
+ isRenewal: boolean;
16
+ productId: string | null;
17
+ newExpirationDate: string | null;
18
+ }
19
+
20
+ /**
21
+ * Detects if a subscription renewal occurred
22
+ *
23
+ * Best Practice (RevenueCat):
24
+ * - Track previous expiration date
25
+ * - If new expiration > previous → Renewal detected
26
+ * - Reset credits on renewal (industry standard)
27
+ *
28
+ * @param state Previous state (expiration date, product ID)
29
+ * @param customerInfo Current CustomerInfo from RevenueCat
30
+ * @param entitlementId Entitlement identifier to check
31
+ * @returns Renewal detection result
32
+ */
33
+ export function detectRenewal(
34
+ state: RenewalState,
35
+ customerInfo: CustomerInfo,
36
+ entitlementId: string
37
+ ): RenewalDetectionResult {
38
+ const entitlement = customerInfo.entitlements.active[entitlementId];
39
+
40
+ if (!entitlement) {
41
+ return {
42
+ isRenewal: false,
43
+ productId: null,
44
+ newExpirationDate: null,
45
+ };
46
+ }
47
+
48
+ const newExpirationDate = entitlement.expirationDate;
49
+ const productId = entitlement.productIdentifier;
50
+
51
+ if (!newExpirationDate || !state.previousExpirationDate) {
52
+ return {
53
+ isRenewal: false,
54
+ productId,
55
+ newExpirationDate,
56
+ };
57
+ }
58
+
59
+ const newExpiration = new Date(newExpirationDate);
60
+ const previousExpiration = new Date(state.previousExpirationDate);
61
+
62
+ const isRenewal = newExpiration > previousExpiration &&
63
+ productId === state.previousProductId;
64
+
65
+ return {
66
+ isRenewal,
67
+ productId,
68
+ newExpirationDate,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Updates renewal state after detection
74
+ */
75
+ export function updateRenewalState(
76
+ _state: RenewalState,
77
+ result: RenewalDetectionResult
78
+ ): RenewalState {
79
+ return {
80
+ previousExpirationDate: result.newExpirationDate,
81
+ previousProductId: result.productId,
82
+ };
83
+ }