@umituz/react-native-subscription 1.4.0 → 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.4.0",
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,74 +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";
74
+
75
+ // =============================================================================
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";
81
88
 
82
89
  // =============================================================================
83
- // UTILS
90
+ // UTILS - Date & Price
84
91
  // =============================================================================
85
92
 
86
- // Date utilities
87
93
  export {
88
94
  isSubscriptionExpired,
89
95
  getDaysUntilExpiration,
90
- } from './utils/dateValidationUtils';
91
-
92
- // Price utilities
93
- export { formatPrice } from './utils/priceUtils';
96
+ } from "./utils/dateValidationUtils";
94
97
 
98
+ export { formatPrice } from "./utils/priceUtils";
95
99
 
96
100
  // =============================================================================
97
- // USER TIER - Types & Utilities
101
+ // UTILS - User Tier
98
102
  // =============================================================================
99
103
 
100
104
  export type {
101
105
  UserTier,
102
106
  UserTierInfo,
103
107
  PremiumStatusFetcher,
104
- } from './utils/types';
108
+ } from "./utils/types";
105
109
 
106
110
  export {
107
111
  getUserTierInfo,
108
112
  checkPremiumAccess,
109
- } from './utils/tierUtils';
113
+ } from "./utils/tierUtils";
110
114
 
111
115
  export {
112
116
  hasTierAccess,
113
117
  isTierPremium,
114
118
  isTierFreemium,
115
119
  isTierGuest,
116
- } from './utils/userTierUtils';
120
+ } from "./utils/userTierUtils";
117
121
 
118
122
  export {
119
123
  isAuthenticated,
120
124
  isGuest,
121
- } from './utils/authUtils';
125
+ } from "./utils/authUtils";
122
126
 
123
127
  export {
124
128
  isValidUserTier,
@@ -127,5 +131,10 @@ export {
127
131
  validateIsGuest,
128
132
  validateIsPremium,
129
133
  validateFetcher,
130
- } from './utils/validation';
134
+ } from "./utils/validation";
135
+
136
+ // =============================================================================
137
+ // TYPES - Re-export from peer dependencies
138
+ // =============================================================================
131
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
+