@umituz/react-native-subscription 1.3.1 → 1.5.0

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "1.3.1",
4
- "description": "Subscription management system for React Native apps - Database-first approach with secure validation",
3
+ "version": "1.5.0",
4
+ "description": "Subscription management and paywall UI for React Native apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
7
7
  "scripts": {
@@ -12,16 +12,12 @@
12
12
  "react-native",
13
13
  "subscription",
14
14
  "premium",
15
+ "paywall",
15
16
  "in-app-purchase",
16
17
  "iap",
17
- "security",
18
+ "revenuecat",
18
19
  "ddd",
19
- "domain-driven-design",
20
- "type-safe",
21
- "solid",
22
- "dry",
23
- "kiss",
24
- "database-first"
20
+ "domain-driven-design"
25
21
  ],
26
22
  "author": "Ümit UZ <umit@umituz.com>",
27
23
  "license": "MIT",
@@ -31,7 +27,12 @@
31
27
  },
32
28
  "peerDependencies": {
33
29
  "react": ">=18.2.0",
34
- "react-native": ">=0.74.0"
30
+ "react-native": ">=0.74.0",
31
+ "react-native-purchases": ">=8.0.0",
32
+ "react-native-safe-area-context": ">=4.0.0",
33
+ "@umituz/react-native-design-system-atoms": "*",
34
+ "@umituz/react-native-design-system-theme": "*",
35
+ "@umituz/react-native-legal": "*"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/react": "~19.1.0",
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Credits Package Entity
3
+ * Represents a credit package for purchase
4
+ */
5
+
6
+ export interface CreditsPackage {
7
+ id: string;
8
+ credits: number;
9
+ price: number;
10
+ priceString?: string;
11
+ currency: string;
12
+ bonus?: number;
13
+ popular?: boolean;
14
+ badge?: string;
15
+ description?: string;
16
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Paywall Tab Entity
3
+ * Represents paywall tab types
4
+ */
5
+
6
+ export type PaywallTabType = "credits" | "subscription";
7
+
8
+ export interface PaywallTab {
9
+ id: PaywallTabType;
10
+ label: string;
11
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Subscription Plan Entity
3
+ * Represents a subscription plan for purchase
4
+ */
5
+
6
+ export interface SubscriptionPlan {
7
+ /** Plan ID */
8
+ id: string;
9
+
10
+ /** Plan type */
11
+ type: "monthly" | "yearly";
12
+
13
+ /** Price */
14
+ price: number;
15
+
16
+ /** Currency code */
17
+ currency: string;
18
+
19
+ /** Whether this is the best value option */
20
+ isBestValue?: boolean;
21
+
22
+ /** Optional discount percentage */
23
+ discountPercentage?: number;
24
+
25
+ /** Optional features list */
26
+ features?: string[];
27
+ }
package/src/index.ts CHANGED
@@ -1,23 +1,12 @@
1
1
  /**
2
2
  * React Native Subscription - Public API
3
3
  *
4
+ * Subscription management and paywall UI for React Native apps
4
5
  * Domain-Driven Design (DDD) Architecture
5
- *
6
- * This is the SINGLE SOURCE OF TRUTH for all subscription operations.
7
- * ALL imports from the Subscription package MUST go through this file.
8
- *
9
- * Architecture:
10
- * - domain: Entities, value objects, errors (business logic)
11
- * - application: Ports (interfaces)
12
- * - infrastructure: Subscription service implementation
13
- * - presentation: Hooks (React integration)
14
- *
15
- * Usage:
16
- * import { initializeSubscriptionService, useSubscription } from '@umituz/react-native-subscription';
17
6
  */
18
7
 
19
8
  // =============================================================================
20
- // DOMAIN LAYER - Business Logic
9
+ // DOMAIN LAYER - Errors
21
10
  // =============================================================================
22
11
 
23
12
  export {
@@ -25,25 +14,29 @@ export {
25
14
  SubscriptionRepositoryError,
26
15
  SubscriptionValidationError,
27
16
  SubscriptionConfigurationError,
28
- } from './domain/errors/SubscriptionError';
17
+ } from "./domain/errors/SubscriptionError";
18
+
19
+ // =============================================================================
20
+ // DOMAIN LAYER - Entities
21
+ // =============================================================================
29
22
 
30
23
  export {
31
24
  createDefaultSubscriptionStatus,
32
25
  isSubscriptionValid,
33
- } from './domain/entities/SubscriptionStatus';
34
- export type { SubscriptionStatus } from './domain/entities/SubscriptionStatus';
26
+ } from "./domain/entities/SubscriptionStatus";
27
+ export type { SubscriptionStatus } from "./domain/entities/SubscriptionStatus";
35
28
 
36
- export type { SubscriptionConfig } from './domain/value-objects/SubscriptionConfig';
29
+ export type { SubscriptionConfig } from "./domain/value-objects/SubscriptionConfig";
37
30
 
38
31
  // =============================================================================
39
32
  // APPLICATION LAYER - Ports
40
33
  // =============================================================================
41
34
 
42
- export type { ISubscriptionRepository } from './application/ports/ISubscriptionRepository';
43
- export type { ISubscriptionService } from './application/ports/ISubscriptionService';
35
+ export type { ISubscriptionRepository } from "./application/ports/ISubscriptionRepository";
36
+ export type { ISubscriptionService } from "./application/ports/ISubscriptionService";
44
37
 
45
38
  // =============================================================================
46
- // INFRASTRUCTURE LAYER - Implementation
39
+ // INFRASTRUCTURE LAYER - Services
47
40
  // =============================================================================
48
41
 
49
42
  export {
@@ -51,71 +44,85 @@ export {
51
44
  initializeSubscriptionService,
52
45
  getSubscriptionService,
53
46
  resetSubscriptionService,
54
- } from './infrastructure/services/SubscriptionService';
47
+ } from "./infrastructure/services/SubscriptionService";
55
48
 
56
49
  // =============================================================================
57
50
  // PRESENTATION LAYER - Hooks
58
51
  // =============================================================================
59
52
 
60
- export { useSubscription } from './presentation/hooks/useSubscription';
61
- export type { UseSubscriptionResult } from './presentation/hooks/useSubscription';
53
+ export { useSubscription } from "./presentation/hooks/useSubscription";
54
+ export type { UseSubscriptionResult } from "./presentation/hooks/useSubscription";
62
55
 
63
56
  export {
64
57
  usePremiumGate,
65
58
  type UsePremiumGateParams,
66
59
  type UsePremiumGateResult,
67
- } from './presentation/hooks/usePremiumGate';
60
+ } from "./presentation/hooks/usePremiumGate";
68
61
 
69
62
  export {
70
63
  useUserTier,
71
64
  type UseUserTierParams,
72
65
  type UseUserTierResult,
73
- } from './presentation/hooks/useUserTier';
66
+ } from "./presentation/hooks/useUserTier";
74
67
 
75
68
  export {
76
69
  useUserTierWithRepository,
77
70
  type UseUserTierWithRepositoryParams,
78
71
  type UseUserTierWithRepositoryResult,
79
72
  type AuthProvider,
80
- } from './presentation/hooks/useUserTierWithRepository';
73
+ } from "./presentation/hooks/useUserTierWithRepository";
81
74
 
82
75
  // =============================================================================
83
- // UTILS
76
+ // PRESENTATION LAYER - Paywall Components
77
+ // =============================================================================
78
+
79
+ export {
80
+ SubscriptionModal,
81
+ type SubscriptionModalProps,
82
+ } from "./presentation/components/paywall/SubscriptionModal";
83
+
84
+ export { SubscriptionPlanCard } from "./presentation/components/paywall/SubscriptionPlanCard";
85
+ export { PaywallFeaturesList } from "./presentation/components/paywall/PaywallFeaturesList";
86
+ export { PaywallFeatureItem } from "./presentation/components/paywall/PaywallFeatureItem";
87
+ export { PaywallLegalFooter } from "./presentation/components/paywall/PaywallLegalFooter";
88
+
89
+ // =============================================================================
90
+ // UTILS - Date & Price
84
91
  // =============================================================================
85
92
 
86
- // Date utilities
87
93
  export {
88
94
  isSubscriptionExpired,
89
95
  getDaysUntilExpiration,
90
- } from './utils/dateValidationUtils';
96
+ } from "./utils/dateValidationUtils";
91
97
 
98
+ export { formatPrice } from "./utils/priceUtils";
92
99
 
93
100
  // =============================================================================
94
- // USER TIER - Types & Utilities
101
+ // UTILS - User Tier
95
102
  // =============================================================================
96
103
 
97
104
  export type {
98
105
  UserTier,
99
106
  UserTierInfo,
100
107
  PremiumStatusFetcher,
101
- } from './utils/types';
108
+ } from "./utils/types";
102
109
 
103
110
  export {
104
111
  getUserTierInfo,
105
112
  checkPremiumAccess,
106
- } from './utils/tierUtils';
113
+ } from "./utils/tierUtils";
107
114
 
108
115
  export {
109
116
  hasTierAccess,
110
117
  isTierPremium,
111
118
  isTierFreemium,
112
119
  isTierGuest,
113
- } from './utils/userTierUtils';
120
+ } from "./utils/userTierUtils";
114
121
 
115
122
  export {
116
123
  isAuthenticated,
117
124
  isGuest,
118
- } from './utils/authUtils';
125
+ } from "./utils/authUtils";
119
126
 
120
127
  export {
121
128
  isValidUserTier,
@@ -124,5 +131,10 @@ export {
124
131
  validateIsGuest,
125
132
  validateIsPremium,
126
133
  validateFetcher,
127
- } from './utils/validation';
134
+ } from "./utils/validation";
135
+
136
+ // =============================================================================
137
+ // TYPES - Re-export from peer dependencies
138
+ // =============================================================================
128
139
 
140
+ export type { PurchasesPackage } from "react-native-purchases";
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Credits Package Card Component
3
+ * Single Responsibility: Display a single credits package option
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
8
+ import { AtomicText } from "@umituz/react-native-design-system-atoms";
9
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
10
+ import type { CreditsPackage } from "../../domain/entities/CreditsPackage";
11
+
12
+ interface CreditsPackageCardProps {
13
+ package: CreditsPackage;
14
+ isSelected: boolean;
15
+ onSelect: () => void;
16
+ }
17
+
18
+ export const CreditsPackageCard: React.FC<CreditsPackageCardProps> =
19
+ React.memo(({ package: pkg, isSelected, onSelect }) => {
20
+ const tokens = useAppDesignTokens();
21
+
22
+ const totalCredits = pkg.credits + (pkg.bonus || 0);
23
+
24
+ return (
25
+ <TouchableOpacity
26
+ style={[
27
+ styles.container,
28
+ {
29
+ backgroundColor: isSelected
30
+ ? tokens.colors.primaryLight
31
+ : tokens.colors.surface,
32
+ borderColor: isSelected
33
+ ? tokens.colors.primary
34
+ : tokens.colors.border,
35
+ borderWidth: isSelected ? 2 : 1,
36
+ },
37
+ ]}
38
+ onPress={onSelect}
39
+ activeOpacity={0.8}
40
+ >
41
+ {pkg.badge && (
42
+ <View
43
+ style={[styles.badge, { backgroundColor: tokens.colors.warning }]}
44
+ >
45
+ <AtomicText
46
+ type="labelSmall"
47
+ style={{ color: tokens.colors.onPrimary, fontWeight: "700" }}
48
+ >
49
+ {pkg.badge}
50
+ </AtomicText>
51
+ </View>
52
+ )}
53
+ <View style={styles.content}>
54
+ <View style={styles.leftSection}>
55
+ <AtomicText
56
+ type="headlineMedium"
57
+ style={[styles.credits, { color: tokens.colors.textPrimary }]}
58
+ >
59
+ {totalCredits.toLocaleString()} Credits
60
+ </AtomicText>
61
+ {(pkg.bonus ?? 0) > 0 && (
62
+ <AtomicText
63
+ type="bodySmall"
64
+ style={[styles.bonus, { color: tokens.colors.success }]}
65
+ >
66
+ +{pkg.bonus} bonus
67
+ </AtomicText>
68
+ )}
69
+ {pkg.description && (
70
+ <AtomicText
71
+ type="bodySmall"
72
+ style={{ color: tokens.colors.textSecondary }}
73
+ >
74
+ {pkg.description}
75
+ </AtomicText>
76
+ )}
77
+ </View>
78
+ <View style={styles.rightSection}>
79
+ <AtomicText
80
+ type="titleLarge"
81
+ style={[styles.price, { color: tokens.colors.primary }]}
82
+ >
83
+ {pkg.currency} {pkg.price.toFixed(2)}
84
+ </AtomicText>
85
+ </View>
86
+ </View>
87
+ </TouchableOpacity>
88
+ );
89
+ });
90
+
91
+ CreditsPackageCard.displayName = "CreditsPackageCard";
92
+
93
+ const styles = StyleSheet.create({
94
+ container: {
95
+ borderRadius: 16,
96
+ padding: 20,
97
+ position: "relative",
98
+ },
99
+ badge: {
100
+ position: "absolute",
101
+ top: -10,
102
+ right: 20,
103
+ paddingHorizontal: 10,
104
+ paddingVertical: 4,
105
+ borderRadius: 8,
106
+ },
107
+ content: {
108
+ flexDirection: "row",
109
+ justifyContent: "space-between",
110
+ alignItems: "center",
111
+ },
112
+ leftSection: {
113
+ flex: 1,
114
+ },
115
+ credits: {
116
+ fontWeight: "700",
117
+ marginBottom: 4,
118
+ },
119
+ bonus: {
120
+ fontWeight: "600",
121
+ marginBottom: 4,
122
+ },
123
+ rightSection: {
124
+ alignItems: "flex-end",
125
+ },
126
+ price: {
127
+ fontWeight: "700",
128
+ },
129
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Credits Tab Content Component
3
+ * Single Responsibility: Display credits packages list
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet, ScrollView } from "react-native";
8
+ import { AtomicText, AtomicButton } from "@umituz/react-native-design-system-atoms";
9
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
10
+ import { useLocalization } from "@umituz/react-native-localization";
11
+ import { CreditsPackageCard } from "./CreditsPackageCard";
12
+ import { PaywallLegalFooter } from "./PaywallLegalFooter";
13
+ import type { CreditsPackage } from "../../domain/entities/CreditsPackage";
14
+
15
+ interface CreditsTabContentProps {
16
+ packages: CreditsPackage[];
17
+ selectedPackageId: string | null;
18
+ onSelectPackage: (packageId: string) => void;
19
+ onPurchase: () => void;
20
+ currentCredits: number;
21
+ requiredCredits?: number;
22
+ isLoading?: boolean;
23
+ purchaseButtonText?: string;
24
+ creditsInfoText?: string;
25
+ processingText?: string;
26
+ }
27
+
28
+ export const CreditsTabContent: React.FC<CreditsTabContentProps> = React.memo(
29
+ ({
30
+ packages,
31
+ selectedPackageId,
32
+ onSelectPackage,
33
+ onPurchase,
34
+ currentCredits,
35
+ requiredCredits,
36
+ isLoading = false,
37
+ purchaseButtonText,
38
+ creditsInfoText,
39
+ processingText,
40
+ }) => {
41
+ const tokens = useAppDesignTokens();
42
+ const { t } = useLocalization();
43
+
44
+ const needsCredits = requiredCredits && requiredCredits > currentCredits;
45
+
46
+ const displayPurchaseButtonText = purchaseButtonText ||
47
+ t("paywall.purchase", { defaultValue: "Purchase" });
48
+ const displayProcessingText = processingText ||
49
+ t("paywall.processing", { defaultValue: "Processing..." });
50
+ const displayCreditsInfoText = creditsInfoText ||
51
+ t("paywall.creditsInfo", { defaultValue: "You need {required} credits. You have {current}." });
52
+
53
+ return (
54
+ <View style={styles.container}>
55
+ {needsCredits && (
56
+ <View
57
+ style={[
58
+ styles.infoCard,
59
+ { backgroundColor: tokens.colors.surfaceSecondary },
60
+ ]}
61
+ >
62
+ <AtomicText
63
+ type="bodyMedium"
64
+ style={{ color: tokens.colors.textSecondary }}
65
+ >
66
+ {displayCreditsInfoText
67
+ .replace("{required}", String(requiredCredits))
68
+ .replace("{current}", String(currentCredits))}
69
+ </AtomicText>
70
+ </View>
71
+ )}
72
+
73
+ <ScrollView
74
+ style={styles.scrollView}
75
+ contentContainerStyle={styles.scrollContent}
76
+ showsVerticalScrollIndicator={false}
77
+ >
78
+ <View style={styles.packagesContainer}>
79
+ {packages.map((pkg) => (
80
+ <CreditsPackageCard
81
+ key={pkg.id}
82
+ package={pkg}
83
+ isSelected={selectedPackageId === pkg.id}
84
+ onSelect={() => onSelectPackage(pkg.id)}
85
+ />
86
+ ))}
87
+ </View>
88
+ </ScrollView>
89
+
90
+ <View style={styles.footer}>
91
+ <AtomicButton
92
+ title={isLoading ? displayProcessingText : displayPurchaseButtonText}
93
+ onPress={onPurchase}
94
+ disabled={!selectedPackageId || isLoading}
95
+ />
96
+ </View>
97
+
98
+ <PaywallLegalFooter />
99
+ </View>
100
+ );
101
+ },
102
+ );
103
+
104
+ CreditsTabContent.displayName = "CreditsTabContent";
105
+
106
+ const styles = StyleSheet.create({
107
+ container: {
108
+ flex: 1,
109
+ },
110
+ infoCard: {
111
+ marginHorizontal: 24,
112
+ marginBottom: 16,
113
+ padding: 12,
114
+ borderRadius: 12,
115
+ },
116
+ scrollView: {
117
+ flex: 1,
118
+ },
119
+ scrollContent: {
120
+ paddingHorizontal: 24,
121
+ paddingBottom: 16,
122
+ },
123
+ packagesContainer: {
124
+ gap: 12,
125
+ },
126
+ footer: {
127
+ padding: 24,
128
+ paddingTop: 16,
129
+ },
130
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Paywall Feature Item Component
3
+ * Single Responsibility: Display a single feature in the features list
4
+ */
5
+
6
+ import React, { useMemo } from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system-atoms";
9
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
10
+
11
+ interface PaywallFeatureItemProps {
12
+ icon: string;
13
+ text: string;
14
+ }
15
+
16
+ export const PaywallFeatureItem: React.FC<PaywallFeatureItemProps> = React.memo(
17
+ ({ icon, text }) => {
18
+ const tokens = useAppDesignTokens();
19
+
20
+ // Icon name should already be PascalCase (e.g., "Sparkles", "Image", "Wand")
21
+ // If not, convert first letter to uppercase
22
+ const iconName = useMemo(() => {
23
+ if (!icon) return "Circle";
24
+ // If already PascalCase, return as is
25
+ if (/^[A-Z]/.test(icon)) {
26
+ return icon;
27
+ }
28
+ // Convert first letter to uppercase
29
+ return icon.charAt(0).toUpperCase() + icon.slice(1);
30
+ }, [icon]);
31
+
32
+ return (
33
+ <View style={styles.featureItem}>
34
+ <AtomicIcon
35
+ name={iconName}
36
+ customSize={20}
37
+ customColor={tokens.colors.primary}
38
+ style={styles.featureIcon}
39
+ />
40
+ <AtomicText
41
+ type="bodyMedium"
42
+ style={[styles.featureText, { color: tokens.colors.text }]}
43
+ >
44
+ {text}
45
+ </AtomicText>
46
+ </View>
47
+ );
48
+ },
49
+ );
50
+
51
+ PaywallFeatureItem.displayName = "PaywallFeatureItem";
52
+
53
+ const styles = StyleSheet.create({
54
+ featureItem: {
55
+ flexDirection: "row",
56
+ alignItems: "center",
57
+ },
58
+ featureIcon: {
59
+ marginRight: 12,
60
+ },
61
+ featureText: {
62
+ fontSize: 15,
63
+ flex: 1,
64
+ lineHeight: 22,
65
+ },
66
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Paywall Features List Component
3
+ * Displays premium features list
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { PaywallFeatureItem } from "./PaywallFeatureItem";
9
+
10
+ interface PaywallFeaturesListProps {
11
+ /** Features list */
12
+ features: Array<{ icon: string; text: string }>;
13
+ /** Optional custom container style */
14
+ containerStyle?: object;
15
+ /** Optional gap between items (default: 12) */
16
+ gap?: number;
17
+ }
18
+
19
+ export const PaywallFeaturesList: React.FC<PaywallFeaturesListProps> = React.memo(
20
+ ({ features, containerStyle, gap = 12 }) => {
21
+ return (
22
+ <View style={[styles.container, containerStyle]}>
23
+ {features.map((feature, index) => (
24
+ <View
25
+ key={`${feature.icon}-${feature.text}-${index}`}
26
+ style={{ marginBottom: index < features.length - 1 ? gap : 0 }}
27
+ >
28
+ <PaywallFeatureItem icon={feature.icon} text={feature.text} />
29
+ </View>
30
+ ))}
31
+ </View>
32
+ );
33
+ },
34
+ );
35
+
36
+ PaywallFeaturesList.displayName = "PaywallFeaturesList";
37
+
38
+ const styles = StyleSheet.create({
39
+ container: {
40
+ width: "100%",
41
+ },
42
+ });
43
+