@umituz/react-native-subscription 2.14.98 → 2.14.99
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/README.md +0 -1
- package/package.json +1 -1
- package/src/domains/README.md +240 -0
- package/src/domains/config/domain/README.md +390 -0
- package/src/domains/config/domain/entities/README.md +350 -0
- package/src/presentation/hooks/useDeductCredit.md +146 -130
- package/src/revenuecat/application/README.md +158 -0
- package/src/revenuecat/application/ports/README.md +169 -0
- package/src/revenuecat/domain/constants/README.md +183 -0
- package/src/revenuecat/domain/entities/README.md +382 -0
- package/src/revenuecat/domain/types/README.md +373 -0
- package/src/revenuecat/domain/value-objects/README.md +441 -0
- package/src/revenuecat/infrastructure/README.md +50 -0
- package/src/revenuecat/infrastructure/handlers/README.md +218 -0
- package/src/revenuecat/infrastructure/services/README.md +325 -0
- package/src/revenuecat/infrastructure/utils/README.md +382 -0
- package/src/revenuecat/presentation/README.md +184 -0
package/README.md
CHANGED
|
@@ -28,7 +28,6 @@ yarn add @umituz/react-native-subscription
|
|
|
28
28
|
"@tanstack/react-query": ">=5.0.0",
|
|
29
29
|
"expo-constants": ">=16.0.0",
|
|
30
30
|
"expo-image": ">=2.0.0",
|
|
31
|
-
"expo-linear-gradient": ">=14.0.0",
|
|
32
31
|
"firebase": ">=10.0.0",
|
|
33
32
|
"react": ">=18.2.0",
|
|
34
33
|
"react-native": ">=0.74.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.99",
|
|
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",
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Domains
|
|
2
|
+
|
|
3
|
+
This directory contains specialized domain modules that implement specific business logic and features.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `domains` directory organizes business logic into focused, self-contained modules. Each domain follows Domain-Driven Design (DDD) principles and Clean Architecture patterns.
|
|
8
|
+
|
|
9
|
+
## Domain Modules
|
|
10
|
+
|
|
11
|
+
### Wallet Domain (`wallet/`)
|
|
12
|
+
|
|
13
|
+
Manages credits, transactions, and wallet operations.
|
|
14
|
+
|
|
15
|
+
**Key Features:**
|
|
16
|
+
- Credit balance tracking
|
|
17
|
+
- Transaction history
|
|
18
|
+
- Credit deduction and allocation
|
|
19
|
+
- Purchase initialization
|
|
20
|
+
- Duplicate protection
|
|
21
|
+
|
|
22
|
+
**Structure:**
|
|
23
|
+
```
|
|
24
|
+
wallet/
|
|
25
|
+
├── domain/ # Business logic and entities
|
|
26
|
+
│ ├── entities/ # UserCredits, Transaction, CreditPackage
|
|
27
|
+
│ ├── types/ # Type definitions
|
|
28
|
+
│ └── mappers/ # Data transformation
|
|
29
|
+
├── infrastructure/ # External integrations
|
|
30
|
+
│ ├── repositories/ # Data persistence
|
|
31
|
+
│ └── services/ # External services
|
|
32
|
+
└── presentation/ # UI layer
|
|
33
|
+
├── hooks/ # React hooks
|
|
34
|
+
├── components/ # React components
|
|
35
|
+
└── screens/ # Screen components
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Key Entities:**
|
|
39
|
+
- `UserCredits`: Credit balance and allocations
|
|
40
|
+
- `Transaction`: Credit transaction records
|
|
41
|
+
- `CreditPackage`: Purchasable credit packages
|
|
42
|
+
|
|
43
|
+
**Allocation Modes:**
|
|
44
|
+
- **ACCUMULATE**: Add credits on renewal (default)
|
|
45
|
+
- **REPLACE**: Replace existing credits with new allocation
|
|
46
|
+
|
|
47
|
+
**Related:**
|
|
48
|
+
- [Wallet Domain README](./wallet/README.md)
|
|
49
|
+
- [Wallet Entities](./wallet/domain/entities/README.md)
|
|
50
|
+
- [Wallet Hooks](./wallet/presentation/hooks/README.md)
|
|
51
|
+
|
|
52
|
+
### Paywall Domain (`paywall/`)
|
|
53
|
+
|
|
54
|
+
Manages paywall display, triggers, and purchase flows.
|
|
55
|
+
|
|
56
|
+
**Key Features:**
|
|
57
|
+
- Paywall visibility management
|
|
58
|
+
- Trigger-based paywall display
|
|
59
|
+
- Package selection and comparison
|
|
60
|
+
- Purchase operations
|
|
61
|
+
- Feedback collection
|
|
62
|
+
|
|
63
|
+
**Structure:**
|
|
64
|
+
```
|
|
65
|
+
paywall/
|
|
66
|
+
├── domain/ # Business logic
|
|
67
|
+
│ └── entities/ # PaywallTrigger, PaywallConfig, PaywallState
|
|
68
|
+
├── components/ # UI components
|
|
69
|
+
│ ├── PaywallScreen/
|
|
70
|
+
│ ├── PackageCard/
|
|
71
|
+
│ └── FeatureComparison/
|
|
72
|
+
└── hooks/ # React hooks
|
|
73
|
+
└── usePaywallOperations/
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Triggers:**
|
|
77
|
+
- `premium_feature`: User attempts premium feature
|
|
78
|
+
- `credit_gate`: Insufficient credits
|
|
79
|
+
- `manual`: Manually triggered
|
|
80
|
+
- `onboarding_complete`: After onboarding
|
|
81
|
+
- `usage_limit`: Reached usage limit
|
|
82
|
+
|
|
83
|
+
**Related:**
|
|
84
|
+
- [Paywall Domain README](./paywall/README.md)
|
|
85
|
+
- [Paywall Entities](./paywall/entities/README.md)
|
|
86
|
+
- [Paywall Components](./paywall/components/README.md)
|
|
87
|
+
|
|
88
|
+
### Config Domain (`config/`)
|
|
89
|
+
|
|
90
|
+
Manages subscription and feature configuration.
|
|
91
|
+
|
|
92
|
+
**Key Features:**
|
|
93
|
+
- Package configuration
|
|
94
|
+
- Feature flags
|
|
95
|
+
- Subscription settings
|
|
96
|
+
- Paywall customization
|
|
97
|
+
|
|
98
|
+
**Structure:**
|
|
99
|
+
```
|
|
100
|
+
config/
|
|
101
|
+
├── domain/ # Configuration entities
|
|
102
|
+
│ ├── entities/ # Config entities
|
|
103
|
+
│ └── value-objects/ # Configuration value objects
|
|
104
|
+
└── utils/ # Configuration utilities
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Related:**
|
|
108
|
+
- [Config Domain README](./config/README.md)
|
|
109
|
+
|
|
110
|
+
## Architecture Principles
|
|
111
|
+
|
|
112
|
+
### 1. Domain-Driven Design (DDD)
|
|
113
|
+
Each domain represents a distinct business capability with:
|
|
114
|
+
- Clear boundaries
|
|
115
|
+
- Ubiquitous language
|
|
116
|
+
- Domain entities
|
|
117
|
+
- Business rules
|
|
118
|
+
|
|
119
|
+
### 2. Clean Architecture
|
|
120
|
+
Each domain follows layered architecture:
|
|
121
|
+
- **Domain Layer**: Business logic, entities (pure)
|
|
122
|
+
- **Application Layer**: Use cases, orchestration
|
|
123
|
+
- **Infrastructure Layer**: External integrations (persistence, APIs)
|
|
124
|
+
- **Presentation Layer**: UI components, hooks
|
|
125
|
+
|
|
126
|
+
### 3. Independence
|
|
127
|
+
Domains are independent and can:
|
|
128
|
+
- Be developed in isolation
|
|
129
|
+
- Have their own data stores
|
|
130
|
+
- Be tested independently
|
|
131
|
+
- Scale independently
|
|
132
|
+
|
|
133
|
+
### 4. Communication
|
|
134
|
+
Domains communicate through:
|
|
135
|
+
- Well-defined interfaces
|
|
136
|
+
- Events (when needed)
|
|
137
|
+
- Shared kernels (when necessary)
|
|
138
|
+
|
|
139
|
+
## Usage Patterns
|
|
140
|
+
|
|
141
|
+
### Using Wallet Domain
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { useCredits } from './wallet/presentation/hooks';
|
|
145
|
+
import { CreditBalanceCard } from './wallet/presentation/components';
|
|
146
|
+
|
|
147
|
+
function MyComponent() {
|
|
148
|
+
const { credits, transactions } = useCredits({ userId: user?.uid });
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<CreditBalanceCard
|
|
152
|
+
credits={credits}
|
|
153
|
+
balance={calculateBalance(credits)}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Using Paywall Domain
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { usePaywallOperations } from './paywall/hooks';
|
|
163
|
+
|
|
164
|
+
function MyScreen() {
|
|
165
|
+
const { handlePurchase } = usePaywallOperations({
|
|
166
|
+
userId: user?.uid,
|
|
167
|
+
isAnonymous: false,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const onBuyPress = () => {
|
|
171
|
+
handlePurchase(selectedPackage);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Using Config Domain
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { getPackageConfiguration } from './config/utils';
|
|
180
|
+
|
|
181
|
+
const packages = getPackageConfiguration('premium');
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Adding New Domains
|
|
185
|
+
|
|
186
|
+
When adding a new domain:
|
|
187
|
+
|
|
188
|
+
1. **Create Domain Structure**
|
|
189
|
+
```bash
|
|
190
|
+
src/domains/your-domain/
|
|
191
|
+
├── domain/
|
|
192
|
+
│ ├── entities/
|
|
193
|
+
│ ├── value-objects/
|
|
194
|
+
│ └── types/
|
|
195
|
+
├── infrastructure/
|
|
196
|
+
│ ├── repositories/
|
|
197
|
+
│ └── services/
|
|
198
|
+
└── presentation/
|
|
199
|
+
├── hooks/
|
|
200
|
+
├── components/
|
|
201
|
+
└── screens/
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
2. **Define Entities**
|
|
205
|
+
- Create core business entities
|
|
206
|
+
- Define business rules
|
|
207
|
+
- Implement validations
|
|
208
|
+
|
|
209
|
+
3. **Implement Infrastructure**
|
|
210
|
+
- Create repositories for data persistence
|
|
211
|
+
- Implement external service integrations
|
|
212
|
+
- Add mappers for data transformation
|
|
213
|
+
|
|
214
|
+
4. **Create Presentation Layer**
|
|
215
|
+
- Implement React hooks
|
|
216
|
+
- Create UI components
|
|
217
|
+
- Build screens
|
|
218
|
+
|
|
219
|
+
5. **Add Documentation**
|
|
220
|
+
- Create README for domain
|
|
221
|
+
- Document entities
|
|
222
|
+
- Provide usage examples
|
|
223
|
+
|
|
224
|
+
## Best Practices
|
|
225
|
+
|
|
226
|
+
1. **Encapsulation**: Keep domain logic isolated
|
|
227
|
+
2. **Purity**: Domain layer should have no external dependencies
|
|
228
|
+
3. **Interfaces**: Depend on abstractions, not concretions
|
|
229
|
+
4. **Testing**: Each domain should be independently testable
|
|
230
|
+
5. **Documentation**: Document business rules and entities
|
|
231
|
+
6. **Type Safety**: Use TypeScript for all domain models
|
|
232
|
+
7. **Validation**: Validate at domain boundaries
|
|
233
|
+
8. **Error Handling**: Use domain-specific errors
|
|
234
|
+
|
|
235
|
+
## Related
|
|
236
|
+
|
|
237
|
+
- [Domain Layer README](../domain/README.md)
|
|
238
|
+
- [Application Layer README](../application/README.md)
|
|
239
|
+
- [Infrastructure Layer README](../infrastructure/README.md)
|
|
240
|
+
- [RevenueCat Integration README](../revenuecat/README.md)
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# Config Domain
|
|
2
|
+
|
|
3
|
+
Domain layer for configuration management.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This directory contains the business logic and domain models for subscription and feature configuration.
|
|
8
|
+
|
|
9
|
+
## Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
domain/
|
|
13
|
+
├── entities/ # Configuration entities
|
|
14
|
+
└── value-objects/ # Configuration value objects
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Entities
|
|
18
|
+
|
|
19
|
+
### PackageConfig
|
|
20
|
+
|
|
21
|
+
Represents a subscription package configuration.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
class PackageConfig {
|
|
25
|
+
readonly identifier: string;
|
|
26
|
+
readonly productId: string;
|
|
27
|
+
readonly period: PackagePeriod;
|
|
28
|
+
readonly price: Money;
|
|
29
|
+
readonly features: Feature[];
|
|
30
|
+
readonly credits?: number;
|
|
31
|
+
readonly metadata: PackageMetadata;
|
|
32
|
+
|
|
33
|
+
constructor(config: PackageConfigData) {
|
|
34
|
+
this.identifier = config.identifier;
|
|
35
|
+
this.productId = config.productId;
|
|
36
|
+
this.period = config.period;
|
|
37
|
+
this.price = new Money(config.price, config.currency);
|
|
38
|
+
this.features = config.features;
|
|
39
|
+
this.credits = config.credits;
|
|
40
|
+
this.metadata = config.metadata || {};
|
|
41
|
+
this.validate();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private validate(): void {
|
|
45
|
+
if (!this.identifier) {
|
|
46
|
+
throw new Error('Package identifier is required');
|
|
47
|
+
}
|
|
48
|
+
if (!this.productId) {
|
|
49
|
+
throw new Error('Product ID is required');
|
|
50
|
+
}
|
|
51
|
+
if (this.price.getAmount() < 0) {
|
|
52
|
+
throw new Error('Price cannot be negative');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isAnnual(): boolean {
|
|
57
|
+
return this.period === 'annual';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
isMonthly(): boolean {
|
|
61
|
+
return this.period === 'monthly';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isLifetime(): boolean {
|
|
65
|
+
return this.period === 'lifetime';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
hasCredits(): boolean {
|
|
69
|
+
return !!this.credits && this.credits > 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
isRecommended(): boolean {
|
|
73
|
+
return this.metadata.recommended === true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
isHighlighted(): boolean {
|
|
77
|
+
return this.metadata.highlight === true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getDiscountPercentage(): number | null {
|
|
81
|
+
return this.metadata.discount?.percentage ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getBadge(): string | null {
|
|
85
|
+
return this.metadata.badge ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getPerMonthPrice(): Money | null {
|
|
89
|
+
if (this.period === 'monthly') {
|
|
90
|
+
return this.price;
|
|
91
|
+
}
|
|
92
|
+
if (this.period === 'annual') {
|
|
93
|
+
return this.price.divide(12);
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### FeatureConfig
|
|
101
|
+
|
|
102
|
+
Represents a feature configuration.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
class FeatureConfig {
|
|
106
|
+
readonly id: string;
|
|
107
|
+
readonly name: string;
|
|
108
|
+
readonly description?: string;
|
|
109
|
+
readonly requiresPremium: boolean;
|
|
110
|
+
readonly requiresCredits: boolean;
|
|
111
|
+
readonly creditCost?: number;
|
|
112
|
+
readonly enabled: boolean;
|
|
113
|
+
readonly gateType: 'premium' | 'credits' | 'both';
|
|
114
|
+
|
|
115
|
+
constructor(config: FeatureConfigData) {
|
|
116
|
+
this.id = config.id;
|
|
117
|
+
this.name = config.name;
|
|
118
|
+
this.description = config.description;
|
|
119
|
+
this.requiresPremium = config.requiresPremium;
|
|
120
|
+
this.requiresCredits = config.requiresCredits ?? false;
|
|
121
|
+
this.creditCost = config.creditCost;
|
|
122
|
+
this.enabled = config.enabled ?? true;
|
|
123
|
+
this.gateType = config.gateType ?? this.inferGateType();
|
|
124
|
+
this.validate();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private inferGateType(): 'premium' | 'credits' | 'both' {
|
|
128
|
+
if (this.requiresPremium && this.requiresCredits) {
|
|
129
|
+
return 'both';
|
|
130
|
+
}
|
|
131
|
+
if (this.requiresPremium) {
|
|
132
|
+
return 'premium';
|
|
133
|
+
}
|
|
134
|
+
return 'credits';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private validate(): void {
|
|
138
|
+
if (!this.id) {
|
|
139
|
+
throw new Error('Feature ID is required');
|
|
140
|
+
}
|
|
141
|
+
if (this.requiresCredits && !this.creditCost) {
|
|
142
|
+
throw new Error('Credit cost required when requiresCredits is true');
|
|
143
|
+
}
|
|
144
|
+
if (this.creditCost && this.creditCost < 0) {
|
|
145
|
+
throw new Error('Credit cost cannot be negative');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
isAccessible(userHasPremium: boolean, userCredits: number): boolean {
|
|
150
|
+
if (!this.enabled) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (this.gateType === 'premium') {
|
|
155
|
+
return userHasPremium;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (this.gateType === 'credits') {
|
|
159
|
+
return userCredits >= (this.creditCost ?? 0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (this.gateType === 'both') {
|
|
163
|
+
return userHasPremium && userCredits >= (this.creditCost ?? 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getRequiredCredits(): number {
|
|
170
|
+
return this.creditCost ?? 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### PaywallConfig
|
|
176
|
+
|
|
177
|
+
Represents paywall configuration.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
class PaywallConfig {
|
|
181
|
+
readonly id: string;
|
|
182
|
+
readonly title: string;
|
|
183
|
+
readonly subtitle?: string;
|
|
184
|
+
readonly features: string[];
|
|
185
|
+
readonly packages: PackageConfig[];
|
|
186
|
+
readonly highlightPackage?: string;
|
|
187
|
+
readonly style: PaywallStyle;
|
|
188
|
+
readonly triggers: PaywallTrigger[];
|
|
189
|
+
|
|
190
|
+
constructor(config: PaywallConfigData) {
|
|
191
|
+
this.id = config.id;
|
|
192
|
+
this.title = config.title;
|
|
193
|
+
this.subtitle = config.subtitle;
|
|
194
|
+
this.features = config.features;
|
|
195
|
+
this.packages = config.packages.map(p => new PackageConfig(p));
|
|
196
|
+
this.highlightPackage = config.highlightPackage;
|
|
197
|
+
this.style = config.style;
|
|
198
|
+
this.triggers = config.triggers ?? [];
|
|
199
|
+
this.validate();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private validate(): void {
|
|
203
|
+
if (!this.id) {
|
|
204
|
+
throw new Error('Paywall ID is required');
|
|
205
|
+
}
|
|
206
|
+
if (this.packages.length === 0) {
|
|
207
|
+
throw new Error('At least one package is required');
|
|
208
|
+
}
|
|
209
|
+
if (this.highlightPackage) {
|
|
210
|
+
const exists = this.packages.some(p => p.identifier === this.highlightPackage);
|
|
211
|
+
if (!exists) {
|
|
212
|
+
throw new Error('Highlight package not found');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getHighlightedPackage(): PackageConfig | null {
|
|
218
|
+
if (!this.highlightPackage) {
|
|
219
|
+
return this.packages.find(p => p.isHighlighted()) ?? null;
|
|
220
|
+
}
|
|
221
|
+
return this.packages.find(p => p.identifier === this.highlightPackage) ?? null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getPackageByIdentifier(identifier: string): PackageConfig | null {
|
|
225
|
+
return this.packages.find(p => p.identifier === identifier) ?? null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
shouldTrigger(triggerType: string): boolean {
|
|
229
|
+
return this.triggers.some(t => t.type === triggerType);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Value Objects
|
|
235
|
+
|
|
236
|
+
### PackagePeriod
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
class PackagePeriod {
|
|
240
|
+
readonly value: 'monthly' | 'annual' | 'lifetime' | 'weekly';
|
|
241
|
+
|
|
242
|
+
constructor(value: string) {
|
|
243
|
+
const valid = ['monthly', 'annual', 'lifetime', 'weekly'];
|
|
244
|
+
if (!valid.includes(value)) {
|
|
245
|
+
throw new Error(`Invalid period: ${value}`);
|
|
246
|
+
}
|
|
247
|
+
this.value = value as PackagePeriod['value'];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
isRecurring(): boolean {
|
|
251
|
+
return this.value === 'monthly' || this.value === 'annual' || this.value === 'weekly';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getDurationInMonths(): number | null {
|
|
255
|
+
switch (this.value) {
|
|
256
|
+
case 'monthly': return 1;
|
|
257
|
+
case 'annual': return 12;
|
|
258
|
+
case 'weekly': return 1 / 4;
|
|
259
|
+
case 'lifetime': return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
toString(): string {
|
|
264
|
+
return this.value;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Money
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
class Money {
|
|
273
|
+
readonly amount: number;
|
|
274
|
+
readonly currency: string;
|
|
275
|
+
|
|
276
|
+
constructor(amount: number, currency: string) {
|
|
277
|
+
if (amount < 0) {
|
|
278
|
+
throw new Error('Amount cannot be negative');
|
|
279
|
+
}
|
|
280
|
+
this.amount = amount;
|
|
281
|
+
this.currency = currency;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
format(locale: string = 'en-US'): string {
|
|
285
|
+
return new Intl.NumberFormat(locale, {
|
|
286
|
+
style: 'currency',
|
|
287
|
+
currency: this.currency,
|
|
288
|
+
}).format(this.amount);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
divide(factor: number): Money {
|
|
292
|
+
return new Money(this.amount / factor, this.currency);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
multiply(factor: number): Money {
|
|
296
|
+
return new Money(this.amount * factor, this.currency);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
equals(other: Money): boolean {
|
|
300
|
+
return this.amount === other.amount && this.currency === other.currency;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Usage
|
|
306
|
+
|
|
307
|
+
### Creating Package Configuration
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { PackageConfig } from './entities/PackageConfig';
|
|
311
|
+
|
|
312
|
+
const monthlyPackage = new PackageConfig({
|
|
313
|
+
identifier: 'premium_monthly',
|
|
314
|
+
productId: 'com.app.premium.monthly',
|
|
315
|
+
period: 'monthly',
|
|
316
|
+
price: 9.99,
|
|
317
|
+
currency: 'USD',
|
|
318
|
+
features: ['Unlimited Access', 'Ad-Free'],
|
|
319
|
+
credits: 100,
|
|
320
|
+
metadata: {
|
|
321
|
+
recommended: true,
|
|
322
|
+
badge: 'Most Popular',
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
console.log(monthlyPackage.isMonthly()); // true
|
|
327
|
+
console.log(monthlyPackage.getPerMonthPrice()?.format()); // '$9.99'
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Creating Feature Configuration
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { FeatureConfig } from './entities/FeatureConfig';
|
|
334
|
+
|
|
335
|
+
const aiFeature = new FeatureConfig({
|
|
336
|
+
id: 'ai_generation',
|
|
337
|
+
name: 'AI Generation',
|
|
338
|
+
description: 'Generate AI content',
|
|
339
|
+
requiresPremium: false,
|
|
340
|
+
requiresCredits: true,
|
|
341
|
+
creditCost: 5,
|
|
342
|
+
enabled: true,
|
|
343
|
+
gateType: 'credits',
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
console.log(aiFeature.isAccessible(false, 10)); // true
|
|
347
|
+
console.log(aiFeature.isAccessible(false, 3)); // false
|
|
348
|
+
console.log(aiFeature.getRequiredCredits()); // 5
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Creating Paywall Configuration
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import { PaywallConfig } from './entities/PaywallConfig';
|
|
355
|
+
|
|
356
|
+
const paywall = new PaywallConfig({
|
|
357
|
+
id: 'premium_upgrade',
|
|
358
|
+
title: 'Upgrade to Premium',
|
|
359
|
+
subtitle: 'Get unlimited access',
|
|
360
|
+
features: ['Feature 1', 'Feature 2'],
|
|
361
|
+
packages: [monthlyPackage, annualPackage],
|
|
362
|
+
highlightPackage: 'premium_annual',
|
|
363
|
+
style: {
|
|
364
|
+
primaryColor: '#007AFF',
|
|
365
|
+
backgroundColor: '#FFFFFF',
|
|
366
|
+
},
|
|
367
|
+
triggers: [
|
|
368
|
+
{ type: 'credit_gate', enabled: true },
|
|
369
|
+
{ type: 'manual', enabled: true },
|
|
370
|
+
],
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const highlighted = paywall.getHighlightedPackage();
|
|
374
|
+
console.log(highlighted?.identifier); // 'premium_annual'
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Best Practices
|
|
378
|
+
|
|
379
|
+
1. **Validation**: Validate in constructor, fail fast
|
|
380
|
+
2. **Immutability**: Keep entities immutable
|
|
381
|
+
3. **Type Safety**: Use TypeScript for compile-time checks
|
|
382
|
+
4. **Business Logic**: Encapsulate business logic in entities
|
|
383
|
+
5. **Equality**: Implement proper equality methods
|
|
384
|
+
6. **Formatting**: Provide formatting methods
|
|
385
|
+
|
|
386
|
+
## Related
|
|
387
|
+
|
|
388
|
+
- [Config Domain](../../README.md)
|
|
389
|
+
- [Config Entities](./entities/README.md)
|
|
390
|
+
- [Config Value Objects](./value-objects/README.md)
|