@umituz/react-native-subscription 2.26.17 → 2.26.18

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.26.17",
3
+ "version": "2.26.18",
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",
@@ -1,19 +1,39 @@
1
1
  /**
2
2
  * PaywallContainer Component
3
3
  * Uses centralized pending purchase state - no local auth handling
4
+ * Apple Guideline 3.1.2 compliant trial display
4
5
  */
5
6
 
6
- import React, { useMemo } from "react";
7
+ import React, { useMemo, useEffect } from "react";
7
8
  import { usePaywallVisibility } from "../../../presentation/hooks/usePaywallVisibility";
8
9
  import { useSubscriptionPackages } from "../../../revenuecat/presentation/hooks/useSubscriptionPackages";
10
+ import { useRevenueCatTrialEligibility } from "../../../revenuecat/presentation/hooks/useRevenueCatTrialEligibility";
9
11
  import { filterPackagesByMode } from "../../../utils/packageFilter";
10
12
  import { createCreditAmountsFromPackages } from "../../../utils/creditMapper";
11
- import { PaywallModal } from "./PaywallModal";
13
+ import { PaywallModal, type TrialEligibilityInfo } from "./PaywallModal";
12
14
  import { usePaywallActions } from "../hooks/usePaywallActions";
13
15
  import type { PaywallContainerProps } from "./PaywallContainer.types";
14
16
 
15
17
  export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
16
- const { userId, translations, mode = "subscription", legalUrls, features, heroImage, bestValueIdentifier, creditsLabel, creditAmounts, packageFilterConfig, source, onPurchaseSuccess, onPurchaseError, onAuthRequired, visible, onClose } = props;
18
+ const {
19
+ userId,
20
+ translations,
21
+ mode = "subscription",
22
+ legalUrls,
23
+ features,
24
+ heroImage,
25
+ bestValueIdentifier,
26
+ creditsLabel,
27
+ creditAmounts,
28
+ packageFilterConfig,
29
+ source,
30
+ onPurchaseSuccess,
31
+ onPurchaseError,
32
+ onAuthRequired,
33
+ visible,
34
+ onClose,
35
+ trialConfig,
36
+ } = props;
17
37
 
18
38
  const { showPaywall, closePaywall, currentSource } = usePaywallVisibility();
19
39
  const isVisible = visible ?? showPaywall;
@@ -22,6 +42,7 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
22
42
  const purchaseSource = source ?? currentSource ?? "settings";
23
43
 
24
44
  const { data: allPackages = [], isLoading } = useSubscriptionPackages(userId ?? undefined);
45
+ const { eligibilityMap, checkEligibility } = useRevenueCatTrialEligibility();
25
46
  const { handlePurchase, handleRestore } = usePaywallActions({
26
47
  userId: userId ?? undefined,
27
48
  source: purchaseSource,
@@ -31,6 +52,35 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
31
52
  onClose: handleClose,
32
53
  });
33
54
 
55
+ // Check trial eligibility only if trialConfig is enabled
56
+ useEffect(() => {
57
+ if (!trialConfig?.enabled) return;
58
+ if (allPackages.length === 0) return;
59
+
60
+ // If specific product IDs are provided, check only those
61
+ // Otherwise, check all packages
62
+ const productIds = trialConfig.eligibleProductIds?.length
63
+ ? [...trialConfig.eligibleProductIds]
64
+ : allPackages.map((pkg) => pkg.product.identifier);
65
+
66
+ checkEligibility(productIds);
67
+ }, [allPackages, checkEligibility, trialConfig?.enabled, trialConfig?.eligibleProductIds]);
68
+
69
+ // Convert eligibility map to format expected by PaywallModal
70
+ // Only process if trial is enabled
71
+ const trialEligibility = useMemo((): Record<string, TrialEligibilityInfo> => {
72
+ if (!trialConfig?.enabled) return {};
73
+
74
+ const result: Record<string, TrialEligibilityInfo> = {};
75
+ for (const [productId, info] of Object.entries(eligibilityMap)) {
76
+ result[productId] = {
77
+ eligible: info.eligible,
78
+ durationDays: trialConfig.durationDays ?? info.trialDurationDays ?? 7,
79
+ };
80
+ }
81
+ return result;
82
+ }, [eligibilityMap, trialConfig?.enabled, trialConfig?.durationDays]);
83
+
34
84
  const { filteredPackages, computedCreditAmounts } = useMemo(() => ({
35
85
  filteredPackages: filterPackagesByMode(allPackages, mode, packageFilterConfig),
36
86
  computedCreditAmounts: mode !== "subscription" && !creditAmounts ? createCreditAmountsFromPackages(allPackages) : creditAmounts
@@ -53,6 +103,8 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
53
103
  creditAmounts={computedCreditAmounts}
54
104
  onPurchase={handlePurchase}
55
105
  onRestore={handleRestore}
106
+ trialEligibility={trialEligibility}
107
+ trialSubtitleText={trialConfig?.enabled ? trialConfig.trialText : undefined}
56
108
  />
57
109
  );
58
110
  };
@@ -8,6 +8,21 @@ import type { PaywallMode, PaywallTranslations, PaywallLegalUrls, SubscriptionFe
8
8
  import type { PackageFilterConfig } from "../../../utils/packageFilter";
9
9
  import type { PurchaseSource } from "../../../domain/entities/Credits";
10
10
 
11
+ /**
12
+ * Trial display configuration
13
+ * Controls how free trial info is displayed (Apple-compliant)
14
+ */
15
+ export interface TrialConfig {
16
+ /** Enable trial display (default: false) */
17
+ readonly enabled: boolean;
18
+ /** Product IDs that have trial offers (if empty, checks all via RevenueCat) */
19
+ readonly eligibleProductIds?: readonly string[];
20
+ /** Trial duration in days (default: 7) */
21
+ readonly durationDays?: number;
22
+ /** Text to show for trial (e.g., "7 days free, then billed") */
23
+ readonly trialText?: string;
24
+ }
25
+
11
26
  export interface PaywallContainerProps {
12
27
  /** User ID for subscription management */
13
28
  readonly userId: string | null;
@@ -43,5 +58,7 @@ export interface PaywallContainerProps {
43
58
  readonly visible?: boolean;
44
59
  /** Callback when paywall is closed */
45
60
  readonly onClose?: () => void;
61
+ /** Trial display configuration (Apple-compliant) */
62
+ readonly trialConfig?: TrialConfig;
46
63
  }
47
64
 
@@ -40,10 +40,12 @@ export interface PaywallModalProps {
40
40
  onRestore?: () => Promise<void | boolean>;
41
41
  /** Trial eligibility map per product ID */
42
42
  trialEligibility?: Record<string, TrialEligibilityInfo>;
43
+ /** Trial subtitle text for PlanCard (e.g., "7 days free, then billed") */
44
+ trialSubtitleText?: string;
43
45
  }
44
46
 
45
47
  export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
46
- const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore, trialEligibility = {} } = props;
48
+ const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore, trialEligibility = {}, trialSubtitleText } = props;
47
49
  const tokens = useAppDesignTokens();
48
50
  const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
49
51
  const [isLocalProcessing, setIsLocalProcessing] = useState(false);
@@ -124,9 +126,10 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
124
126
 
125
127
  <View style={styles.header}>
126
128
  <AtomicText type="headlineMedium" style={[styles.title, { color: tokens.colors.textPrimary }]}>{translations.title}</AtomicText>
127
- {(translations.trialSubtitle || translations.subtitle) && (
129
+ {/* Apple compliance: Don't promote trial in header, show regular subtitle only */}
130
+ {translations.subtitle && (
128
131
  <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
129
- {translations.trialSubtitle ?? translations.subtitle}
132
+ {translations.subtitle}
130
133
  </AtomicText>
131
134
  )}
132
135
  </View>
@@ -152,8 +155,7 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
152
155
  creditAmount={creditAmounts?.[productId]}
153
156
  creditsLabel={creditsLabel}
154
157
  hasFreeTrial={hasFreeTrial}
155
- trialDurationDays={eligibility?.durationDays}
156
- trialBadgeText={hasFreeTrial ? translations.trialBadgeText : undefined}
158
+ trialSubtitleText={hasFreeTrial ? trialSubtitleText : undefined}
157
159
  />
158
160
  );
159
161
  })}
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Plan Card
3
- * Subscription plan selection 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
4
9
  */
5
10
 
6
11
  import React from "react";
@@ -14,26 +19,22 @@ interface PlanCardProps {
14
19
  pkg: PurchasesPackage;
15
20
  isSelected: boolean;
16
21
  onSelect: () => void;
22
+ /** Badge text (e.g., "Best Value") - NOT for trial */
17
23
  badge?: string;
18
24
  creditAmount?: number;
19
25
  creditsLabel?: string;
20
- /** Whether this plan has a free trial */
26
+ /** Whether this plan has a free trial (Apple-compliant display) */
21
27
  hasFreeTrial?: boolean;
22
- /** Trial duration in days */
23
- trialDurationDays?: number;
24
- /** Trial badge text (e.g., "7 days free") */
25
- trialBadgeText?: string;
28
+ /** Trial subtitle text (e.g., "7 days free, then billed") - shown as small gray text */
29
+ trialSubtitleText?: string;
26
30
  }
27
31
 
28
32
  export const PlanCard: React.FC<PlanCardProps> = React.memo(
29
- ({ pkg, isSelected, onSelect, badge, creditAmount, creditsLabel, hasFreeTrial, trialBadgeText }) => {
33
+ ({ pkg, isSelected, onSelect, badge, creditAmount, creditsLabel, hasFreeTrial, trialSubtitleText }) => {
30
34
  const tokens = useAppDesignTokens();
31
35
  const title = pkg.product.title;
32
36
  const price = formatPrice(pkg.product.price, pkg.product.currencyCode);
33
37
 
34
- // Determine which badge to show (trial badge takes priority if eligible)
35
- const displayBadge = hasFreeTrial && trialBadgeText ? trialBadgeText : badge;
36
-
37
38
  return (
38
39
  <TouchableOpacity onPress={onSelect} activeOpacity={0.7} style={styles.touchable}>
39
40
  <View
@@ -46,13 +47,10 @@ export const PlanCard: React.FC<PlanCardProps> = React.memo(
46
47
  },
47
48
  ]}
48
49
  >
49
- {displayBadge && (
50
+ {/* Badge for "Best Value" etc. - NOT for trial (Apple compliance) */}
51
+ {badge && (
50
52
  <View style={styles.badgeContainer}>
51
- <AtomicBadge
52
- text={displayBadge}
53
- variant={hasFreeTrial ? "success" : "primary"}
54
- size="sm"
55
- />
53
+ <AtomicBadge text={badge} variant="primary" size="sm" />
56
54
  </View>
57
55
  )}
58
56
 
@@ -76,17 +74,38 @@ export const PlanCard: React.FC<PlanCardProps> = React.memo(
76
74
  <AtomicText type="titleSmall" style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}>
77
75
  {title}
78
76
  </AtomicText>
77
+
78
+ {/* Credits info */}
79
79
  {creditAmount && creditsLabel && (
80
80
  <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
81
81
  {creditAmount} {creditsLabel}
82
82
  </AtomicText>
83
83
  )}
84
+
85
+ {/* Trial info - Apple-compliant: small, gray, subordinate */}
86
+ {hasFreeTrial && trialSubtitleText && (
87
+ <AtomicText
88
+ type="bodySmall"
89
+ style={{
90
+ color: tokens.colors.textTertiary ?? tokens.colors.textSecondary,
91
+ fontSize: 11,
92
+ marginTop: 2,
93
+ }}
94
+ >
95
+ {trialSubtitleText}
96
+ </AtomicText>
97
+ )}
84
98
  </View>
85
99
  </View>
86
100
 
101
+ {/* Price - MOST PROMINENT (Apple compliance) */}
87
102
  <AtomicText
88
103
  type="titleMedium"
89
- style={{ color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary, fontWeight: "700" }}
104
+ style={{
105
+ color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary,
106
+ fontWeight: "700",
107
+ fontSize: 18,
108
+ }}
90
109
  >
91
110
  {price}
92
111
  </AtomicText>
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  export { PaywallContainer } from "./PaywallContainer";
6
- export type { PaywallContainerProps } from "./PaywallContainer.types";
6
+ export type { PaywallContainerProps, TrialConfig } from "./PaywallContainer.types";
7
7
 
8
8
  export { PaywallModal } from "./PaywallModal";
9
9
  export type { PaywallModalProps } from "./PaywallModal";
@@ -41,10 +41,6 @@ export interface PaywallTranslations {
41
41
  privacyText?: string;
42
42
  termsOfServiceText?: string;
43
43
  bestValueBadgeText?: string;
44
- /** Trial-related translations */
45
- trialBadgeText?: string;
46
- /** Trial subtitle (e.g., "Try free for 7 days, then $X/year") */
47
- trialSubtitle?: string;
48
44
  }
49
45
 
50
46
  export interface PaywallLegalUrls {
@@ -13,10 +13,6 @@ interface PaywallTranslationKeys {
13
13
  processingText: string;
14
14
  privacyText: string;
15
15
  termsOfServiceText: string;
16
- /** Trial badge text key */
17
- trialBadgeText?: string;
18
- /** Trial subtitle key */
19
- trialSubtitle?: string;
20
16
  }
21
17
 
22
18
  interface UsePaywallTranslationsParams {
@@ -42,8 +38,6 @@ const DEFAULT_KEYS: PaywallTranslationKeys = {
42
38
  processingText: "paywall.processing",
43
39
  privacyText: "auth.privacyPolicy",
44
40
  termsOfServiceText: "auth.termsOfService",
45
- trialBadgeText: "paywall.trial.badge",
46
- trialSubtitle: "paywall.trial.subtitle",
47
41
  };
48
42
 
49
43
  export const usePaywallTranslations = ({
@@ -68,8 +62,6 @@ export const usePaywallTranslations = ({
68
62
  processingText: t(mergedKeys.processingText),
69
63
  privacyText: t(mergedKeys.privacyText),
70
64
  termsOfServiceText: t(mergedKeys.termsOfServiceText),
71
- trialBadgeText: mergedKeys.trialBadgeText ? t(mergedKeys.trialBadgeText) : undefined,
72
- trialSubtitle: mergedKeys.trialSubtitle ? t(mergedKeys.trialSubtitle) : undefined,
73
65
  }),
74
66
  [t, mergedKeys],
75
67
  );
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Global type declarations for React Native environment
3
+ */
4
+
5
+ /** React Native development flag */
6
+ declare const __DEV__: boolean;
7
+
8
+ /** Extend NodeJS namespace for React Native compatibility */
9
+ declare namespace NodeJS {
10
+ interface Global {
11
+ __DEV__: boolean;
12
+ }
13
+ }
@@ -56,14 +56,21 @@ export function getPremiumEntitlement(
56
56
  };
57
57
  }
58
58
 
59
+ /**
60
+ * Type guard for RevenueCat purchase error
61
+ */
62
+ function isRevenueCatPurchaseError(error: unknown): error is RevenueCatPurchaseErrorInfo {
63
+ return error instanceof Error && "userCancelled" in error;
64
+ }
65
+
59
66
  /**
60
67
  * Check if error is a user cancellation
61
68
  */
62
69
  export function isUserCancelledError(error: unknown): boolean {
63
- if (!error || typeof error !== "object") {
70
+ if (!isRevenueCatPurchaseError(error)) {
64
71
  return false;
65
72
  }
66
- return (error as RevenueCatPurchaseErrorInfo).userCancelled === true;
73
+ return error.userCancelled === true;
67
74
  }
68
75
 
69
76
  /**
@@ -76,34 +83,3 @@ export function getErrorMessage(error: unknown, fallback: string): string {
76
83
  return fallback;
77
84
  }
78
85
 
79
- /**
80
- * Trial Eligibility Types
81
- * For RevenueCat introductory offer (free trial) support
82
- */
83
-
84
- /** Trial info for a subscription product */
85
- export interface TrialInfo {
86
- /** Whether user is eligible for trial */
87
- eligible: boolean;
88
- /** Trial duration in days */
89
- durationDays: number;
90
- /** Product identifier */
91
- productId: string;
92
- }
93
-
94
- /** Configuration for trial display */
95
- export interface TrialDisplayConfig {
96
- /** Product IDs that have trial offers */
97
- trialProductIds: string[];
98
- /** Default trial duration in days */
99
- defaultTrialDays: number;
100
- /** Whether to show trial badge */
101
- showTrialBadge: boolean;
102
- }
103
-
104
- /** Default trial configuration */
105
- export const DEFAULT_TRIAL_CONFIG: TrialDisplayConfig = {
106
- trialProductIds: [],
107
- defaultTrialDays: 7,
108
- showTrialBadge: true,
109
- };