react-native-nami-sdk 3.3.7 → 3.3.9-dev.202603131926
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/ARCHITECTURE.md +166 -0
- package/CHANGELOG.md +1 -0
- package/android/build.gradle +3 -3
- package/android/src/main/java/com/namiml/reactnative/NamiCampaignManagerBridge.kt +2 -2
- package/dist/src/transformers.d.ts +4 -1
- package/dist/src/version.d.ts +1 -1
- package/jest.config.ts +10 -0
- package/package.json +5 -10
- package/react-native-nami-sdk.podspec +1 -1
- package/src/NamiCampaignManager.ts +1 -11
- package/src/NamiEntitlementManager.ts +1 -10
- package/src/__tests__/transformers.test.ts +177 -0
- package/src/transformers.ts +21 -1
- package/src/version.ts +1 -1
- package/tsconfig.json +2 -0
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# React Native SDK Architecture
|
|
2
|
+
|
|
3
|
+
The Nami React Native SDK (`react-native-nami-sdk`) is a TypeScript bridge layer that exposes native Android and iOS SDK functionality to React Native applications via TurboModules.
|
|
4
|
+
|
|
5
|
+
## Build System
|
|
6
|
+
|
|
7
|
+
- **Language:** TypeScript 5 (strict mode), Kotlin (Android bridge), Swift (iOS bridge)
|
|
8
|
+
- **Package Manager:** npm
|
|
9
|
+
- **TypeScript Output:** Declaration files only (`emitDeclarationOnly`)
|
|
10
|
+
- **Native Code Generation:** React Native Codegen (TurboModule specs)
|
|
11
|
+
- **Linting:** ESLint with @react-native config, Prettier
|
|
12
|
+
|
|
13
|
+
### Native Dependencies
|
|
14
|
+
|
|
15
|
+
| Platform | SDK |
|
|
16
|
+
|----------|-----|
|
|
17
|
+
| Android (Google Play) | `com.namiml:sdk-android` |
|
|
18
|
+
| Android (Amazon) | `com.namiml:sdk-amazon` |
|
|
19
|
+
| iOS/tvOS | `Nami` CocoaPod |
|
|
20
|
+
|
|
21
|
+
## Directory Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
sdk/react-native/
|
|
25
|
+
├── src/ # TypeScript bridge layer
|
|
26
|
+
│ ├── index.ts # Main exports
|
|
27
|
+
│ ├── types.ts # Type definitions
|
|
28
|
+
│ ├── transformers.ts # Data transformation utilities
|
|
29
|
+
│ ├── version.ts # Auto-generated version constant
|
|
30
|
+
│ ├── Nami.ts # Core SDK initialization
|
|
31
|
+
│ ├── NamiPaywallManager.ts # Paywall display/events
|
|
32
|
+
│ ├── NamiCampaignManager.ts # Campaign management
|
|
33
|
+
│ ├── NamiCustomerManager.ts # Customer identity
|
|
34
|
+
│ ├── NamiEntitlementManager.ts # Entitlement access
|
|
35
|
+
│ ├── NamiPurchaseManager.ts # Purchase operations
|
|
36
|
+
│ ├── NamiFlowManager.ts # Flow management
|
|
37
|
+
│ └── NamiOverlayControl.tsx # React component for overlay UI
|
|
38
|
+
├── specs/ # TurboModule native interface specs
|
|
39
|
+
├── android/ # Kotlin native bridge modules
|
|
40
|
+
│ └── src/main/java/com/namiml/reactnative/
|
|
41
|
+
├── ios/ # Swift + Objective-C native bridge
|
|
42
|
+
├── dist/ # Built .d.ts type declarations
|
|
43
|
+
├── examples/
|
|
44
|
+
│ ├── Basic/ # Full-featured sample (with Detox e2e)
|
|
45
|
+
│ └── TestNamiTV/ # tvOS sample
|
|
46
|
+
├── build-utils/ # Version management scripts
|
|
47
|
+
└── scripts/ # Version generation
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Bridge Architecture
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
JavaScript (React Native)
|
|
54
|
+
|
|
|
55
|
+
TurboModule TypeScript Specs (/specs)
|
|
56
|
+
|
|
|
57
|
+
Native Bridge Modules (Kotlin + Swift)
|
|
58
|
+
|
|
|
59
|
+
Native SDKs (com.namiml:sdk-android / Nami CocoaPod)
|
|
60
|
+
|
|
|
61
|
+
Platform APIs (Google Play Billing / StoreKit)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The React Native SDK is a **thin bridge** - it does NOT reimplement networking, IAP handling, paywall rendering, entitlement validation, or purchase tracking. All business logic lives in the native SDKs.
|
|
65
|
+
|
|
66
|
+
## Manager Classes
|
|
67
|
+
|
|
68
|
+
Each TypeScript manager wraps a corresponding TurboModule:
|
|
69
|
+
|
|
70
|
+
| Manager | Key Methods |
|
|
71
|
+
|---------|-------------|
|
|
72
|
+
| `Nami` | `configure()`, `sdkConfigured()`, `sdkVersion()` |
|
|
73
|
+
| `NamiPaywallManager` | `buySkuComplete()`, handler registration (buy, close, sign-in, restore, deeplink) |
|
|
74
|
+
| `NamiCampaignManager` | `launch()`, `allCampaigns()`, `isCampaignAvailable()`, `isFlow()`, `refresh()`, `getProductGroups()` |
|
|
75
|
+
| `NamiCustomerManager` | `login()`, `logout()`, `journeyState()`, `setCustomerAttribute()`, `setAnonymousMode()`, `deviceId()` |
|
|
76
|
+
| `NamiEntitlementManager` | `active()`, `isEntitlementActive()`, `refresh()`, `clearProvisionalEntitlementGrants()` |
|
|
77
|
+
| `NamiPurchaseManager` | `allPurchases()`, `skuPurchased()`, `restorePurchases()`, `presentCodeRedemptionSheet()` |
|
|
78
|
+
| `NamiFlowManager` | `pause()`, `resume()`, `finish()`, `isFlowOpen()`, `registerStepHandoff()` |
|
|
79
|
+
| `NamiOverlayControl` | `presentOverlay()`, `finishOverlay()`, React `NamiOverlayHost` component |
|
|
80
|
+
|
|
81
|
+
## Native Bridge Modules
|
|
82
|
+
|
|
83
|
+
### Android (`android/` - Kotlin)
|
|
84
|
+
|
|
85
|
+
- **NamiBridgePackage.java** - TurboReactPackage registering all modules
|
|
86
|
+
- **NamiBridgeModule.kt** - `RNNami` core configuration
|
|
87
|
+
- **NamiCampaignManagerBridgeModule.kt** - Campaign bridge
|
|
88
|
+
- **NamiPaywallManagerBridgeModule.kt** - Paywall bridge
|
|
89
|
+
- **NamiPurchaseManagerBridge.kt** - Purchase bridge
|
|
90
|
+
- **NamiEntitlementManagerBridge.kt** - Entitlement bridge
|
|
91
|
+
- **NamiCustomerManagerBridge.kt** - Customer bridge
|
|
92
|
+
- **NamiFlowManagerBridge.kt** - Flow bridge
|
|
93
|
+
- **NamiOverlayControlBridge.kt** - Overlay + ReactOverlayActivity
|
|
94
|
+
- **NamiUtil.kt** - Data transformation (WritableMap/WritableArray)
|
|
95
|
+
|
|
96
|
+
Android supports two product flavors: `play` (Google Play) and `amazon` (Amazon Appstore).
|
|
97
|
+
|
|
98
|
+
### iOS (`ios/` - Swift)
|
|
99
|
+
|
|
100
|
+
Parallel Swift implementations for each bridge module, plus Objective-C `.m` files for Codegen/legacy architecture support.
|
|
101
|
+
|
|
102
|
+
## Type System (`src/types.ts`)
|
|
103
|
+
|
|
104
|
+
Key types exported to consumers:
|
|
105
|
+
|
|
106
|
+
- **NamiConfiguration** - SDK init params (appPlatformID, logLevel, language)
|
|
107
|
+
- **NamiSKU** - Product details with platform-specific pricing (Apple, Google, Amazon)
|
|
108
|
+
- **NamiPurchase** - Purchase record with timestamps, transaction IDs, source
|
|
109
|
+
- **NamiEntitlement** - Entitlement with active purchases and related SKUs
|
|
110
|
+
- **NamiCampaign** - Campaign metadata (name, rule type, form factors, segment)
|
|
111
|
+
- **CustomerJourneyState** - Subscription lifecycle flags
|
|
112
|
+
- **NamiPaywallEvent** - Comprehensive event (30+ actions)
|
|
113
|
+
- **NamiPaywallAction** - Enum: BUY_SKU, SELECT_SKU, RESTORE_PURCHASES, VIDEO_STARTED, etc.
|
|
114
|
+
|
|
115
|
+
## Testing
|
|
116
|
+
|
|
117
|
+
### Unit Tests
|
|
118
|
+
|
|
119
|
+
TypeScript unit tests using Jest + ts-jest, covering the pure data transformation layer in `src/transformers.ts`:
|
|
120
|
+
|
|
121
|
+
- **`parsePurchaseDates`** — converts native timestamp numbers to JS Date objects
|
|
122
|
+
- **`coerceSkuType`** — validates raw strings against `NamiSKUType` union, falls back to `'unknown'`
|
|
123
|
+
- **`mapToNamiPaywallAction`** — validates raw strings against the `NamiPaywallAction` enum (28 values), falls back to `UNKNOWN`
|
|
124
|
+
- **`parseEntitlements`** — transforms raw entitlement arrays, parsing nested purchase dates and defaulting missing SKU arrays
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# From monorepo root
|
|
128
|
+
make test-react-native
|
|
129
|
+
|
|
130
|
+
# Or directly
|
|
131
|
+
cd sdk/react-native && npm test
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Test files live in `src/__tests__/` and are excluded from TypeScript compilation output via `tsconfig.json`.
|
|
135
|
+
|
|
136
|
+
### E2E Tests
|
|
137
|
+
|
|
138
|
+
- Detox framework in `examples/Basic/e2e/` (iOS simulator + Android emulator)
|
|
139
|
+
- CI via GitHub Actions with artifact upload on failure
|
|
140
|
+
|
|
141
|
+
#### Local Native SDK Resolution
|
|
142
|
+
|
|
143
|
+
In the monorepo, native SDKs may not yet be published when e2e tests run against a PR. The workflow resolves this by building native SDKs locally before the Detox build:
|
|
144
|
+
|
|
145
|
+
**iOS** — The `NAMI_SDK_LOCAL_PATH` environment variable points to `sdk/apple/` in the monorepo. When set, each example app's Podfile overrides the published `Nami` CocoaPod with a local `:path` reference. CI builds the XCFramework first (`make build-apple`), then sets the env var so `pod install` picks up the local artifact.
|
|
146
|
+
|
|
147
|
+
**Android** — CI publishes the SDK to `mavenLocal` via `./gradlew sdk:publishPublicGooglePublicationToMavenLocal sdk:publishPublicAmazonPublicationToMavenLocal`. The example app's `build.gradle` already includes `mavenLocal()` in its repository list, so Gradle resolves the local artifact without any code changes.
|
|
148
|
+
|
|
149
|
+
Both approaches are temporary until dev release publishing is in place, at which point the pre-build steps and env var can be removed.
|
|
150
|
+
|
|
151
|
+
### Code Quality
|
|
152
|
+
|
|
153
|
+
- ESLint + TypeScript compilation checks in CI
|
|
154
|
+
|
|
155
|
+
## Peer Dependencies
|
|
156
|
+
|
|
157
|
+
- React >= 18
|
|
158
|
+
- React Native >= 0.73
|
|
159
|
+
|
|
160
|
+
## Architectural Patterns
|
|
161
|
+
|
|
162
|
+
- **Bridge/Adapter** - TypeScript wrappers over native TurboModules
|
|
163
|
+
- **Event Emitter** - NativeEventEmitter for real-time native-to-JS events
|
|
164
|
+
- **Thin Client** - All business logic delegated to native SDKs
|
|
165
|
+
- **Codegen** - TurboModule specs for typed native module generation (New Architecture)
|
|
166
|
+
- **Transformer** - Data conversion between native types and JS objects
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Changelog - React Native SDK
|
package/android/build.gradle
CHANGED
|
@@ -15,7 +15,7 @@ apply plugin: 'com.android.library'
|
|
|
15
15
|
// apply plugin: 'maven'
|
|
16
16
|
apply plugin: "kotlin-android"
|
|
17
17
|
buildscript {
|
|
18
|
-
ext.kotlin_version = '1.
|
|
18
|
+
ext.kotlin_version = '2.1.0'
|
|
19
19
|
// The Android Gradle plugin is only required when opening the android folder stand-alone.
|
|
20
20
|
// This avoids unnecessary downloads and potential conflicts when the library is included as a
|
|
21
21
|
// module dependency in an application project.
|
|
@@ -85,8 +85,8 @@ dependencies {
|
|
|
85
85
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
86
86
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
|
87
87
|
|
|
88
|
-
playImplementation "com.namiml:sdk-android:3.3.
|
|
89
|
-
amazonImplementation "com.namiml:sdk-amazon:3.3.
|
|
88
|
+
playImplementation "com.namiml:sdk-android:3.3.9-dev.202603131926"
|
|
89
|
+
amazonImplementation "com.namiml:sdk-amazon:3.3.9-dev.202603131926"
|
|
90
90
|
|
|
91
91
|
implementation "com.facebook.react:react-native:+" // From node_modules
|
|
92
92
|
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
|
|
@@ -140,9 +140,9 @@ class NamiCampaignManagerBridgeModule internal constructor(
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
if (context.hasKey("productGroups")) {
|
|
143
|
-
paywallLaunchContext = PaywallLaunchContext(productGroups.toList(), customAttributes, customObject)
|
|
143
|
+
paywallLaunchContext = PaywallLaunchContext(productGroups = productGroups.toList(), customAttributes = customAttributes, customObject = customObject)
|
|
144
144
|
} else {
|
|
145
|
-
paywallLaunchContext = PaywallLaunchContext(null, customAttributes, customObject)
|
|
145
|
+
paywallLaunchContext = PaywallLaunchContext(productGroups = null, customAttributes = customAttributes, customObject = customObject)
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
import type { NamiPurchase, NamiSKUType } from './types';
|
|
1
|
+
import type { NamiEntitlement, NamiPurchase, NamiSKUType } from './types';
|
|
2
|
+
import { NamiPaywallAction } from './types';
|
|
2
3
|
export declare function parsePurchaseDates(purchase: any): NamiPurchase;
|
|
3
4
|
export declare function coerceSkuType(raw: string): NamiSKUType;
|
|
5
|
+
export declare function mapToNamiPaywallAction(action: string): NamiPaywallAction;
|
|
6
|
+
export declare function parseEntitlements(entitlements: any[]): NamiEntitlement[];
|
package/dist/src/version.d.ts
CHANGED
package/jest.config.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nami-sdk",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.9-dev.202603131926",
|
|
4
4
|
"description": "React Native SDK for Nami - No-code paywall and onboarding flows with A/B testing.",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"build": "tsc",
|
|
24
24
|
"generate:version": "ts-node scripts/generate-version.ts",
|
|
25
25
|
"prepare": "npm run generate:version && npm run build",
|
|
26
|
-
"test": "
|
|
26
|
+
"test": "jest",
|
|
27
27
|
"android": "react-native run-android",
|
|
28
28
|
"android-clean": "cd android && rm -rf .gradle && rm -rf .idea && rm -rf android.iml && rm -rf local.properties",
|
|
29
29
|
"ios": "react-native run-ios",
|
|
@@ -73,19 +73,14 @@
|
|
|
73
73
|
"eslint": "^8.57.1",
|
|
74
74
|
"eslint-plugin-prettier": "^5.5.0",
|
|
75
75
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
76
|
+
"jest": "^30.2.0",
|
|
76
77
|
"prettier": "^3.6.0",
|
|
77
78
|
"react": "^18.2.0",
|
|
78
79
|
"react-native": "^0.73.0",
|
|
79
80
|
"react-native-codegen": "^0.0.12",
|
|
81
|
+
"ts-jest": "^29.4.6",
|
|
80
82
|
"ts-node": "^10.9.2",
|
|
81
83
|
"typescript": "^5.0.2"
|
|
82
84
|
},
|
|
83
|
-
"
|
|
84
|
-
"type": "git",
|
|
85
|
-
"url": "git+https://github.com/namiml/react-native-nami-sdk.git"
|
|
86
|
-
},
|
|
87
|
-
"homepage": "https://www.namiml.com",
|
|
88
|
-
"bugs": {
|
|
89
|
-
"url": "https://github.com/namiml/react-native-nami-sdk/issues"
|
|
90
|
-
}
|
|
85
|
+
"homepage": "https://www.nami.ml"
|
|
91
86
|
}
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
NamiCampaign,
|
|
11
11
|
NamiError,
|
|
12
12
|
} from './types';
|
|
13
|
-
import {
|
|
13
|
+
import { mapToNamiPaywallAction } from './transformers';
|
|
14
14
|
|
|
15
15
|
const RNNamiCampaignManager: Spec =
|
|
16
16
|
TurboModuleRegistry.getEnforcing<Spec>('RNNamiCampaignManager') ??
|
|
@@ -21,16 +21,6 @@ export enum NamiCampaignManagerEvents {
|
|
|
21
21
|
NamiPaywallEvent = 'NamiPaywallEvent',
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const validPaywallActions = new Set(
|
|
25
|
-
Object.values(NamiPaywallAction) as NamiPaywallAction[],
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
function mapToNamiPaywallAction(action: string): NamiPaywallAction {
|
|
29
|
-
return validPaywallActions.has(action as NamiPaywallAction)
|
|
30
|
-
? (action as NamiPaywallAction)
|
|
31
|
-
: NamiPaywallAction.UNKNOWN;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
24
|
const emitter = new NativeEventEmitter(NativeModules.RNNamiCampaignManager);
|
|
35
25
|
|
|
36
26
|
export const NamiCampaignManager = {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from 'react-native';
|
|
7
7
|
import type { Spec } from '../specs/NativeNamiEntitlementManager';
|
|
8
8
|
import type { NamiEntitlement } from './types';
|
|
9
|
-
import {
|
|
9
|
+
import { parseEntitlements } from './transformers';
|
|
10
10
|
|
|
11
11
|
const RNNamiEntitlementManager: Spec =
|
|
12
12
|
TurboModuleRegistry.getEnforcing?.<Spec>('RNNamiEntitlementManager') ??
|
|
@@ -18,15 +18,6 @@ export enum NamiEntitlementManagerEvents {
|
|
|
18
18
|
EntitlementsChanged = 'EntitlementsChanged',
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function parseEntitlements(entitlements: any[]): NamiEntitlement[] {
|
|
22
|
-
return entitlements.map((ent) => ({
|
|
23
|
-
...ent,
|
|
24
|
-
activePurchases: ent.activePurchases.map(parsePurchaseDates),
|
|
25
|
-
relatedSkus: ent.relatedSkus ?? [],
|
|
26
|
-
purchasedSkus: ent.purchasedSkus ?? [],
|
|
27
|
-
}));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
21
|
export const NamiEntitlementManager = {
|
|
31
22
|
emitter,
|
|
32
23
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parsePurchaseDates,
|
|
3
|
+
coerceSkuType,
|
|
4
|
+
mapToNamiPaywallAction,
|
|
5
|
+
parseEntitlements,
|
|
6
|
+
} from '../transformers';
|
|
7
|
+
import { NamiPaywallAction } from '../types';
|
|
8
|
+
|
|
9
|
+
describe('parsePurchaseDates', () => {
|
|
10
|
+
it('converts timestamp numbers to Date objects', () => {
|
|
11
|
+
const raw = {
|
|
12
|
+
skuId: 'com.example.monthly',
|
|
13
|
+
purchaseInitiatedTimestamp: 1700000000000,
|
|
14
|
+
expires: 1700086400000,
|
|
15
|
+
};
|
|
16
|
+
const result = parsePurchaseDates(raw);
|
|
17
|
+
|
|
18
|
+
expect(result.purchaseInitiatedTimestamp).toBeInstanceOf(Date);
|
|
19
|
+
expect(result.purchaseInitiatedTimestamp.getTime()).toBe(1700000000000);
|
|
20
|
+
expect(result.expires).toBeInstanceOf(Date);
|
|
21
|
+
expect(result.expires!.getTime()).toBe(1700086400000);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('sets expires to undefined when absent', () => {
|
|
25
|
+
const raw = {
|
|
26
|
+
skuId: 'com.example.lifetime',
|
|
27
|
+
purchaseInitiatedTimestamp: 1700000000000,
|
|
28
|
+
};
|
|
29
|
+
const result = parsePurchaseDates(raw);
|
|
30
|
+
|
|
31
|
+
expect(result.expires).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('sets expires to undefined when explicitly null', () => {
|
|
35
|
+
const raw = {
|
|
36
|
+
skuId: 'com.example.lifetime',
|
|
37
|
+
purchaseInitiatedTimestamp: 1700000000000,
|
|
38
|
+
expires: null,
|
|
39
|
+
};
|
|
40
|
+
const result = parsePurchaseDates(raw);
|
|
41
|
+
|
|
42
|
+
expect(result.expires).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('preserves all other fields via spread', () => {
|
|
46
|
+
const raw = {
|
|
47
|
+
skuId: 'com.example.monthly',
|
|
48
|
+
transactionIdentifier: 'txn_123',
|
|
49
|
+
purchaseSource: 'CAMPAIGN' as const,
|
|
50
|
+
purchaseInitiatedTimestamp: 1700000000000,
|
|
51
|
+
sku: { id: 'sku_1', skuId: 'com.example.monthly', type: 'subscription' },
|
|
52
|
+
};
|
|
53
|
+
const result = parsePurchaseDates(raw);
|
|
54
|
+
|
|
55
|
+
expect(result.skuId).toBe('com.example.monthly');
|
|
56
|
+
expect(result.transactionIdentifier).toBe('txn_123');
|
|
57
|
+
expect(result.purchaseSource).toBe('CAMPAIGN');
|
|
58
|
+
expect(result.sku).toEqual(raw.sku);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('coerceSkuType', () => {
|
|
63
|
+
it.each(['unknown', 'one_time_purchase', 'subscription'] as const)(
|
|
64
|
+
'accepts valid type "%s"',
|
|
65
|
+
(type) => {
|
|
66
|
+
expect(coerceSkuType(type)).toBe(type);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
it('returns "unknown" for invalid types', () => {
|
|
71
|
+
expect(coerceSkuType('invalid')).toBe('unknown');
|
|
72
|
+
expect(coerceSkuType('')).toBe('unknown');
|
|
73
|
+
expect(coerceSkuType('SUBSCRIPTION')).toBe('unknown');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('mapToNamiPaywallAction', () => {
|
|
78
|
+
it.each(Object.values(NamiPaywallAction))(
|
|
79
|
+
'accepts valid action "%s"',
|
|
80
|
+
(action) => {
|
|
81
|
+
expect(mapToNamiPaywallAction(action)).toBe(action);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
it('returns UNKNOWN for unrecognized actions', () => {
|
|
86
|
+
expect(mapToNamiPaywallAction('NOT_A_REAL_ACTION')).toBe(
|
|
87
|
+
NamiPaywallAction.UNKNOWN,
|
|
88
|
+
);
|
|
89
|
+
expect(mapToNamiPaywallAction('')).toBe(NamiPaywallAction.UNKNOWN);
|
|
90
|
+
expect(mapToNamiPaywallAction('buy_sku')).toBe(NamiPaywallAction.UNKNOWN);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('parseEntitlements', () => {
|
|
95
|
+
const rawPurchase = {
|
|
96
|
+
skuId: 'com.example.monthly',
|
|
97
|
+
purchaseInitiatedTimestamp: 1700000000000,
|
|
98
|
+
expires: 1700086400000,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
it('converts activePurchases dates', () => {
|
|
102
|
+
const raw = [
|
|
103
|
+
{
|
|
104
|
+
name: 'Premium',
|
|
105
|
+
desc: 'Premium access',
|
|
106
|
+
referenceId: 'premium',
|
|
107
|
+
activePurchases: [rawPurchase],
|
|
108
|
+
purchasedSkus: [{ id: 'sku_1', skuId: 'com.example.monthly', type: 'subscription' }],
|
|
109
|
+
relatedSkus: [],
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
const result = parseEntitlements(raw);
|
|
113
|
+
|
|
114
|
+
expect(result).toHaveLength(1);
|
|
115
|
+
expect(result[0].activePurchases[0].purchaseInitiatedTimestamp).toBeInstanceOf(Date);
|
|
116
|
+
expect(result[0].activePurchases[0].expires).toBeInstanceOf(Date);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('defaults relatedSkus and purchasedSkus to empty arrays when missing', () => {
|
|
120
|
+
const raw = [
|
|
121
|
+
{
|
|
122
|
+
name: 'Basic',
|
|
123
|
+
desc: 'Basic access',
|
|
124
|
+
referenceId: 'basic',
|
|
125
|
+
activePurchases: [],
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
const result = parseEntitlements(raw);
|
|
129
|
+
|
|
130
|
+
expect(result[0].relatedSkus).toEqual([]);
|
|
131
|
+
expect(result[0].purchasedSkus).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('preserves existing relatedSkus and purchasedSkus', () => {
|
|
135
|
+
const sku = { id: 'sku_1', skuId: 'com.example.monthly', type: 'subscription' };
|
|
136
|
+
const raw = [
|
|
137
|
+
{
|
|
138
|
+
name: 'Premium',
|
|
139
|
+
desc: 'Premium access',
|
|
140
|
+
referenceId: 'premium',
|
|
141
|
+
activePurchases: [],
|
|
142
|
+
purchasedSkus: [sku],
|
|
143
|
+
relatedSkus: [sku],
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
const result = parseEntitlements(raw);
|
|
147
|
+
|
|
148
|
+
expect(result[0].purchasedSkus).toEqual([sku]);
|
|
149
|
+
expect(result[0].relatedSkus).toEqual([sku]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('handles empty array', () => {
|
|
153
|
+
expect(parseEntitlements([])).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('handles multiple entitlements', () => {
|
|
157
|
+
const raw = [
|
|
158
|
+
{
|
|
159
|
+
name: 'A',
|
|
160
|
+
desc: '',
|
|
161
|
+
referenceId: 'a',
|
|
162
|
+
activePurchases: [rawPurchase],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'B',
|
|
166
|
+
desc: '',
|
|
167
|
+
referenceId: 'b',
|
|
168
|
+
activePurchases: [rawPurchase, rawPurchase],
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
const result = parseEntitlements(raw);
|
|
172
|
+
|
|
173
|
+
expect(result).toHaveLength(2);
|
|
174
|
+
expect(result[0].activePurchases).toHaveLength(1);
|
|
175
|
+
expect(result[1].activePurchases).toHaveLength(2);
|
|
176
|
+
});
|
|
177
|
+
});
|
package/src/transformers.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { NamiPurchase, NamiSKUType } from './types';
|
|
1
|
+
import type { NamiEntitlement, NamiPurchase, NamiSKUType } from './types';
|
|
2
|
+
import { NamiPaywallAction } from './types';
|
|
2
3
|
|
|
3
4
|
export function parsePurchaseDates(purchase: any): NamiPurchase {
|
|
4
5
|
return {
|
|
@@ -19,3 +20,22 @@ export function coerceSkuType(raw: string): NamiSKUType {
|
|
|
19
20
|
? (raw as NamiSKUType)
|
|
20
21
|
: 'unknown';
|
|
21
22
|
}
|
|
23
|
+
|
|
24
|
+
const validPaywallActions = new Set(
|
|
25
|
+
Object.values(NamiPaywallAction) as NamiPaywallAction[],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export function mapToNamiPaywallAction(action: string): NamiPaywallAction {
|
|
29
|
+
return validPaywallActions.has(action as NamiPaywallAction)
|
|
30
|
+
? (action as NamiPaywallAction)
|
|
31
|
+
: NamiPaywallAction.UNKNOWN;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseEntitlements(entitlements: any[]): NamiEntitlement[] {
|
|
35
|
+
return entitlements.map((ent) => ({
|
|
36
|
+
...ent,
|
|
37
|
+
activePurchases: ent.activePurchases.map(parsePurchaseDates),
|
|
38
|
+
relatedSkus: ent.relatedSkus ?? [],
|
|
39
|
+
purchasedSkus: ent.purchasedSkus ?? [],
|
|
40
|
+
}));
|
|
41
|
+
}
|
package/src/version.ts
CHANGED