react-native-nami-sdk 3.0.25 → 3.0.26

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/.eslintignore CHANGED
@@ -1,2 +1,2 @@
1
1
  node_modules/
2
- .eslintrc.js
2
+ .eslintrc
package/.eslintrc.js CHANGED
@@ -3,10 +3,50 @@ module.exports = {
3
3
  extends: '@react-native-community',
4
4
  parser: '@typescript-eslint/parser',
5
5
  plugins: ['@typescript-eslint'],
6
+ parserOptions: {
7
+ ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
8
+ sourceType: 'module', // Allows for the use of imports
9
+ ecmaFeatures: {
10
+ jsx: true, // Allows for the parsing of JSX
11
+ },
12
+ },
6
13
  rules: {
7
14
  'react/no-unstable-nested-components': [
8
15
  'off' | 'warn' | 'error',
9
16
  { allowAsProps: true | false },
10
17
  ],
18
+ 'prettier/prettier': [
19
+ 'error',
20
+ {
21
+ bracketSpacing: true,
22
+ jsxBracketSameLine: true,
23
+ singleQuote: true,
24
+ trailingComma: 'all',
25
+ arrowParens: 'avoid',
26
+ },
27
+ ],
28
+ quotes: ['error', 'single'],
29
+ indent: ['error', 2, { SwitchCase: 1 }],
30
+ 'comma-dangle': [2, 'always-multiline'],
31
+ 'react/prop-types': 'off',
32
+ '@typescript-eslint/no-namespace': 'off',
33
+ '@typescript-eslint/no-explicit-any': 'off',
34
+ '@typescript-eslint/ban-ts-comment': 'off',
35
+ '@typescript-eslint/interface-name-prefix': 'off',
36
+ '@typescript-eslint/no-non-null-assertion': 'off',
37
+ '@typescript-eslint/no-unused-vars': [
38
+ 2,
39
+ { args: 'none', ignoreRestSiblings: true },
40
+ ],
41
+ '@typescript-eslint/no-use-before-define': ['error', { variables: false }],
42
+ 'react/jsx-first-prop-new-line': [1, 'multiline'],
43
+ 'react/jsx-max-props-per-line': [
44
+ 1,
45
+ {
46
+ maximum: 1,
47
+ },
48
+ ],
49
+ 'no-unused-vars': 'off',
50
+ 'object-curly-spacing': ['error', 'always'],
11
51
  },
12
52
  };
@@ -16,14 +16,13 @@ jobs:
16
16
  node-version: "16"
17
17
 
18
18
  - name: 'Checkout ${{ inputs.ref }}'
19
- uses: actions/checkout@v2
19
+ uses: actions/checkout@v3
20
20
  with:
21
21
  path: source
22
22
  ref: '${{ inputs.ref }}'
23
23
 
24
24
  - name: Install Basic app dependencies
25
- run: |
26
- yarn install
25
+ run: yarn --frozen-lockfile --prefer-offline
27
26
  working-directory: source/examples/Basic
28
27
 
29
28
  - name: Linter Basic
@@ -32,11 +31,249 @@ jobs:
32
31
  working-directory: source/examples/Basic
33
32
 
34
33
  - name: Install TestNamiTV app dependencies
35
- run: |
36
- yarn install
34
+ run: yarn --frozen-lockfile --prefer-offline
37
35
  working-directory: source/examples/TestNamiTV
38
36
 
39
37
  - name: Linter TestNamiTV
40
38
  run: |
41
39
  npx eslint . --ext .js,.jsx,.ts,.tsx
42
40
  working-directory: source/examples/TestNamiTV
41
+ e2e-ios:
42
+ runs-on: macos-12
43
+ env:
44
+ DETOX_CONFIGURATION: ios.sim.release
45
+
46
+ steps:
47
+ - uses: actions/setup-node@v3
48
+ with:
49
+ node-version: 16
50
+
51
+ - name: 'Checkout ${{ inputs.ref }}'
52
+ uses: actions/checkout@v3
53
+ with:
54
+ path: source
55
+ ref: '${{ inputs.ref }}'
56
+
57
+ - name: Cache node_modules
58
+ uses: actions/cache@v3
59
+ id: cache
60
+ with:
61
+ path: source/examples/Basic/node_modules
62
+ key: node-modules-${{ hashFiles('**/yarn.lock') }}
63
+
64
+ - name: Install Yarn Dependencies
65
+ run: yarn install
66
+ working-directory: source/examples/Basic
67
+
68
+ - name: Install macOS dependencies
69
+ run: |
70
+ brew tap wix/brew
71
+ brew install applesimutils
72
+ sudo gem install cocoapods
73
+ gem install CFPropertyList
74
+ env:
75
+ # Speed up build could be updated 0/1 for full install
76
+ HOMEBREW_NO_AUTO_UPDATE: 1
77
+ HOMEBREW_NO_INSTALL_CLEANUP: 1
78
+
79
+ - name: Update App Platform ID
80
+ working-directory: source/examples/Basic/config/
81
+ run: |
82
+ sed -i '' -e "s/APPLE_PROD_APP_PLATFORM_ID/$BASIC_APPLE_PROD_APP_PLATFORM_ID/" index.ts
83
+ env:
84
+ BASIC_APPLE_PROD_APP_PLATFORM_ID: '${{ secrets.APPLE_PROD_APP_PLATFORM_ID }}'
85
+
86
+ # - name: Cache Pods
87
+ # uses: actions/cache@v3
88
+ # id: podcache
89
+ # with:
90
+ # path: source/examples/Basic/ios
91
+ # key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
92
+ # restore-keys: |
93
+ # ${{ runner.os }}-pods-
94
+
95
+ - name: Clean node_modules tree
96
+ run: rm -rf node_modules/react-native-nami-sdk/examples
97
+ working-directory: source/examples/Basic
98
+
99
+ - name: Install iOS pods
100
+ run: pod install
101
+ working-directory: source/examples/Basic/ios
102
+
103
+ - name: Rebuild cache detox
104
+ run: yarn detox rebuild-framework-cache
105
+ working-directory: source/examples/Basic
106
+
107
+ - name: Cache Detox build
108
+ id: cache-detox-build
109
+ uses: actions/cache@v3
110
+ with:
111
+ path: source/examples/Basic/ios/Pods
112
+ key: ${{ runner.os }}-detox-build
113
+ restore-keys: |
114
+ ${{ runner.os }}-detox-build
115
+
116
+ - name: Detox build
117
+ run: yarn detox build --configuration ${{ env.DETOX_CONFIGURATION }}
118
+ working-directory: source/examples/Basic
119
+
120
+ - name: Detox test
121
+ run: |
122
+ yarn start &
123
+ METRO_BUNDLER_PID=$!
124
+ yarn detox test --configuration ${{ env.DETOX_CONFIGURATION }} e2e/ios --cleanup --headless --record-logs all
125
+ DETOX_EXIT_CODE=$?
126
+ kill $METRO_BUNDLER_PID
127
+ exit $DETOX_EXIT_CODE
128
+ working-directory: source/examples/Basic
129
+
130
+ - name: Upload artifacts
131
+ if: failure()
132
+ uses: actions/upload-artifact@v3
133
+ with:
134
+ name: detox-artifacts
135
+ path: artifacts
136
+ e2e-android:
137
+ runs-on: macos-latest
138
+ env:
139
+ DETOX_CONFIGURATION: android.emu.release
140
+
141
+ steps:
142
+ - name: Checkout repository
143
+ uses: actions/checkout@v3
144
+ with:
145
+ path: source
146
+ ref: '${{ inputs.ref }}'
147
+
148
+ - name: Setup Node.js
149
+ uses: actions/setup-node@v3
150
+ with:
151
+ node-version: "16"
152
+
153
+ - name: Cache node_modules
154
+ uses: actions/cache@v3
155
+ id: cache
156
+ with:
157
+ path: source/examples/Basic/node_modules
158
+ key: node-modules-${{ hashFiles('**/yarn.lock') }}
159
+
160
+ - name: Install Yarn dependencies
161
+ run: yarn install
162
+ working-directory: source/examples/Basic
163
+
164
+ - name: Setup Java
165
+ uses: actions/setup-java@v3
166
+ with:
167
+ cache: gradle
168
+ distribution: temurin
169
+ java-version: 11
170
+
171
+ - name: Update App Platform ID
172
+ working-directory: source/examples/Basic/config/
173
+ run: |
174
+ sed -i '' -e "s/ANDROID_PROD_APP_PLATFORM_ID/$BASIC_ANDROID_PROD_APP_PLATFORM_ID/" index.ts
175
+ env:
176
+ BASIC_ANDROID_PROD_APP_PLATFORM_ID: '${{ secrets.ANDROID_PROD_APP_PLATFORM_ID }}'
177
+
178
+ - name: Rebuild cache detox
179
+ run: yarn detox rebuild-framework-cache
180
+ working-directory: source/examples/Basic
181
+
182
+ - name: Cache Detox build
183
+ id: cache-detox-build
184
+ uses: actions/cache@v3
185
+ with:
186
+ path: source/examples/Basic/android/app/build
187
+ key: ${{ runner.os }}-detox-build
188
+ restore-keys: |
189
+ ${{ runner.os }}-detox-build
190
+
191
+ - name: Detox build
192
+ run: |
193
+ rm -rf node_modules/react-native-nami-sdk/examples
194
+ yarn detox build --configuration ${{ env.DETOX_CONFIGURATION }}
195
+ working-directory: source/examples/Basic
196
+
197
+ - name: Create the Keystore
198
+ run: |
199
+ # import keystore from secrets
200
+ echo $KEYSTORE_BASE64 | base64 -d > $RUNNER_TEMP/my_production.keystore
201
+ env:
202
+ KEYSTORE_BASE64: '${{ secrets.KEY_STORE_BASE64 }}'
203
+
204
+ - name: Encode the keystore to base64
205
+ id: encode_keystore
206
+ run: |
207
+ echo "SIGNINGKEYBASE64=$(openssl base64 < $RUNNER_TEMP/my_production.keystore | tr -d '\n')" >> $GITHUB_ENV
208
+
209
+ - name: Sign APK
210
+ id: sign_apk
211
+ uses: r0adkll/sign-android-release@v1
212
+ with:
213
+ releaseDirectory: source/examples/Basic/android/app/build/outputs/apk/production/release
214
+ signingKeyBase64: ${{ env.SIGNINGKEYBASE64 }}
215
+ alias: ${{ secrets.KEY_ALIAS }}
216
+ keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
217
+ keyPassword: ${{ secrets.KEY_PASSWORD }}
218
+
219
+ - name: Sign AndroidTest APK for Detox
220
+ id: sign_androidTest_apk
221
+ uses: r0adkll/sign-android-release@v1
222
+ with:
223
+ releaseDirectory: source/examples/Basic/android/app/build/outputs/apk/androidTest/production/release/
224
+ signingKeyBase64: ${{ env.SIGNINGKEYBASE64 }}
225
+ alias: ${{ secrets.KEY_ALIAS }}
226
+ keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
227
+ keyPassword: ${{ secrets.KEY_PASSWORD }}
228
+
229
+ - name: Get device name
230
+ id: device
231
+ run: node -e "console.log('AVD_NAME=' + require('./.detoxrc').devices.emulator.device.avdName)" >> $GITHUB_OUTPUT
232
+ working-directory: source/examples/Basic
233
+
234
+ # - name: Cache AVD snapshot
235
+ # uses: actions/cache@v3
236
+ # id: avd-cache
237
+ # with:
238
+ # path: |
239
+ # ~/.android/avd/*
240
+ # ~/.android/adb*
241
+ # key: avd-30-aosp-atd
242
+ #
243
+ # - name: Create AVD and generate snapshot for caching
244
+ # if: steps.avd-cache.outputs.cache-hit != 'true'
245
+ # uses: reactivecircus/android-emulator-runner@v2
246
+ # with:
247
+ # target: aosp_atd
248
+ # api-level: 30
249
+ # arch: x86
250
+ # channel: canary
251
+ # profile: pixel
252
+ # ram-size: 2048M
253
+ # heapSize: 576M
254
+ # # avd-name: ${{ steps.device.outputs.AVD_NAME }}
255
+ # avd-name: Pixel_3a_API_30_AOSP
256
+ # force-avd-creation: false
257
+ # emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
258
+ # disable-animations: false
259
+ # script: echo "Generated AVD snapshot for caching"
260
+ # working-directory: source/examples/Basic
261
+
262
+ - name: Detox test
263
+ uses: reactivecircus/android-emulator-runner@v2
264
+ with:
265
+ target: aosp_atd
266
+ api-level: 30
267
+ arch: x86
268
+ channel: canary
269
+ profile: pixel
270
+ avd-name: Pixel_4_API_30_AOSP
271
+ script: yarn detox test --configuration ${{ env.DETOX_CONFIGURATION }} e2e/android --headless --record-logs all
272
+ working-directory: source/examples/Basic
273
+
274
+ - name: Upload artifacts
275
+ if: failure()
276
+ uses: actions/upload-artifact@v3
277
+ with:
278
+ name: detox-artifacts
279
+ path: artifacts
package/.prettierrc.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ bracketSpacing: true,
3
+ jsxBracketSameLine: true,
4
+ singleQuote: true,
5
+ trailingComma: 'all',
6
+ arrowParens: 'avoid',
7
+ };
@@ -115,7 +115,7 @@ class NamiBridgeModule(reactContext: ReactApplicationContext) :
115
115
  } else {
116
116
  Arguments.createArray()
117
117
  }
118
- val settingsList = mutableListOf("extendedClientInfo:react-native:3.0.25")
118
+ val settingsList = mutableListOf("extendedClientInfo:react-native:3.0.26")
119
119
  namiCommandsReact?.toArrayList()?.filterIsInstance<String>()?.let { commandsFromReact ->
120
120
  settingsList.addAll(commandsFromReact)
121
121
  }
package/index.d.ts CHANGED
@@ -1,20 +1,8 @@
1
- export {Nami, NamiConfiguration, NamiLanguageCodes} from './src/Nami';
2
- export {NamiMLManager} from './src/NamiMLManager';
3
- export {
4
- NamiCampaignManager,
5
- NamiCampaign,
6
- NamiCampaignRule,
7
- LaunchCampaignError,
8
- } from './src/NamiCampaignManager';
9
- export {
10
- NamiCustomerManager,
11
- CustomerJourneyState,
12
- AccountStateAction,
13
- } from './src/NamiCustomerManager';
14
- export {
15
- NamiEntitlementManager,
16
- NamiEntitlement,
17
- } from './src/NamiEntitlementManager';
18
- export {NamiPurchaseManager, NamiPurchase} from './src/NamiPurchaseManager';
19
- export {NamiPaywallManager} from './src/NamiPaywallManager';
20
- export {NamiSKU} from './src/types';
1
+ export { Nami } from './src/Nami';
2
+ export { NamiMLManager } from './src/NamiMLManager';
3
+ export { NamiCampaignManager } from './src/NamiCampaignManager';
4
+ export { NamiCustomerManager } from './src/NamiCustomerManager';
5
+ export { NamiEntitlementManager } from './src/NamiEntitlementManager';
6
+ export { NamiPurchaseManager } from './src/NamiPurchaseManager';
7
+ export { NamiPaywallManager } from './src/NamiPaywallManager';
8
+ export * from './src/types';
package/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export { Nami } from './src/Nami';
2
+ export { NamiMLManager } from './src/NamiMLManager';
3
+ export { NamiCampaignManager } from './src/NamiCampaignManager';
4
+ export { NamiCustomerManager } from './src/NamiCustomerManager';
5
+ export { NamiEntitlementManager } from './src/NamiEntitlementManager';
6
+ export { NamiPurchaseManager } from './src/NamiPurchaseManager';
7
+ export { NamiPaywallManager } from './src/NamiPaywallManager';
8
+ export * from './src/types';
package/ios/Nami.m CHANGED
@@ -52,7 +52,7 @@ RCT_EXPORT_METHOD(configure: (NSDictionary *)configDict completion: (RCTResponse
52
52
  }
53
53
 
54
54
  // Start commands with header iformation for Nami to let them know this is a React client.
55
- NSMutableArray *namiCommandStrings = [NSMutableArray arrayWithArray:@[@"extendedClientInfo:react-native:3.0.25"]];
55
+ NSMutableArray *namiCommandStrings = [NSMutableArray arrayWithArray:@[@"extendedClientInfo:react-native:3.0.26"]];
56
56
 
57
57
  // Add additional namiCommands app may have sent in.
58
58
  NSObject *appCommandStrings = configDict[@"namiCommands"];
@@ -97,7 +97,7 @@ class RNNamiCampaignManager: RCTEventEmitter {
97
97
  "segmentId": paywallEvent.segmentId,
98
98
  "externalSegmentId": paywallEvent.externalSegmentId,
99
99
  "action": actionString,
100
- "skuId": paywallEvent.sku?.id,
100
+ "skuId": paywallEvent.sku?.skuId,
101
101
  "purchaseError": errorSting,
102
102
  "purchases": dictionaries,
103
103
  "deeplinkUrl": paywallEvent.deeplinkUrl,
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "react-native-nami-sdk",
3
- "version": "3.0.25",
3
+ "version": "3.0.26",
4
4
  "description": "React Native Module for Nami - Easy subscriptions & in-app purchases, with powerful built-in paywalls and A/B testing.",
5
- "main": "index.js",
5
+ "main": "index.ts",
6
+ "types": "index.d.ts",
6
7
  "scripts": {
7
8
  "test": "echo \"Error: no test specified\" && exit 1",
8
9
  "android": "react-native run-android",
@@ -53,8 +54,11 @@
53
54
  },
54
55
  "devDependencies": {
55
56
  "@react-native-community/eslint-config": "^3.2.0",
57
+ "@types/react-native": "^0.72.2",
56
58
  "@typescript-eslint/eslint-plugin": "^5.59.7",
57
59
  "eslint": "^8.41.0",
60
+ "eslint-plugin-jest": "^27.2.2",
61
+ "@types/jest": "^29.5.2",
58
62
  "prettier": "^2.8.7",
59
63
  "react": "^17.0.2",
60
64
  "react-native": "^0.65.2",
package/src/Nami.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { NativeModules } from 'react-native';
2
+ import { NamiConfiguration } from './types';
3
+
4
+ export const { NamiBridge } = NativeModules;
5
+
6
+ export interface INami {
7
+ configure: (
8
+ config: NamiConfiguration,
9
+ resultCallback?: (resultObject: { success: boolean }) => void,
10
+ ) => void;
11
+ }
12
+
13
+ export const Nami: INami = {
14
+ ...NamiBridge,
15
+ configure: (
16
+ configureObj: NamiConfiguration,
17
+ resultCallback?: (resultObject: { success: boolean }) => void,
18
+ ) => {
19
+ NamiBridge.configure(configureObj, resultCallback ?? (() => {}));
20
+ },
21
+ };
@@ -2,6 +2,7 @@ import { EmitterSubscription } from "react-native";
2
2
  import { NamiPurchase } from "./NamiPurchaseManager";
3
3
  import { NamiPaywallAction } from "./NamiPaywallManager";
4
4
  import { NamiSKU } from "./types";
5
+ import { NamiPaywallAction, NamiPurchase } from "./types";
5
6
 
6
7
  export const NamiCampaignManager: {
7
8
  allCampaigns: () => Promise<Array<NamiCampaign>>;
@@ -32,7 +33,7 @@ export const NamiCampaignManager: {
32
33
  ) => void;
33
34
  refresh: () => void;
34
35
  registerAvailableCampaignsHandler: (
35
- callback: (availableCampaigns: NamiCampaign[]) => void
36
+ callback: (availableCampaigns: NamiCampaign[]) => void
36
37
  ) => EmitterSubscription["remove"];
37
38
  };
38
39
 
@@ -0,0 +1,143 @@
1
+ import {
2
+ NativeModules,
3
+ NativeEventEmitter,
4
+ EmitterSubscription,
5
+ } from 'react-native';
6
+ import {
7
+ LaunchCampaignError,
8
+ NamiCampaign,
9
+ NamiPaywallAction,
10
+ NamiPurchase,
11
+ PaywallLaunchContext,
12
+ } from './types';
13
+
14
+ export const { RNNamiCampaignManager } = NativeModules;
15
+
16
+ export enum NamiCampaignManagerEvents {
17
+ ResultCampaign = 'ResultCampaign',
18
+ AvailableCampaignsChanged = 'AvailableCampaignsChanged',
19
+ }
20
+
21
+ const searchString_Nami = 'NAMI_';
22
+
23
+ interface ICampaignManager {
24
+ launchSubscription: EmitterSubscription | undefined;
25
+ emitter: NativeEventEmitter;
26
+ allCampaigns: () => Promise<Array<NamiCampaign>>;
27
+ isCampaignAvailable(campaignSource: string | null): Promise<boolean>;
28
+ launch: (
29
+ label?: string,
30
+ withUrl?: string,
31
+ context?: PaywallLaunchContext,
32
+ resultCallback?: (success: boolean, error?: LaunchCampaignError) => void,
33
+ actionCallback?: (
34
+ action: NamiPaywallAction,
35
+ campaignId: string,
36
+ paywallId: string,
37
+ campaignName?: string,
38
+ campaignType?: string,
39
+ campaignLabel?: string,
40
+ campaignUrl?: string,
41
+ paywallName?: string,
42
+ segmentId?: string,
43
+ externalSegmentId?: string,
44
+ deeplinkUrl?: string,
45
+ skuId?: string,
46
+ componentChangeId?: string,
47
+ componentChangeName?: string,
48
+ purchaseError?: string,
49
+ purchases?: NamiPurchase[],
50
+ ) => void,
51
+ ) => void;
52
+ refresh: () => void;
53
+ registerAvailableCampaignsHandler: (
54
+ callback: (availableCampaigns: NamiCampaign[]) => void,
55
+ ) => EmitterSubscription['remove'];
56
+ }
57
+
58
+ export const NamiCampaignManager: ICampaignManager = {
59
+ launchSubscription: undefined,
60
+ emitter: new NativeEventEmitter(RNNamiCampaignManager),
61
+ ...RNNamiCampaignManager,
62
+ launch(label, withUrl, context, resultCallback, actionCallback) {
63
+ if (this.launchSubscription) {
64
+ this.launchSubscription.remove();
65
+ }
66
+
67
+ this.launchSubscription = this.emitter.addListener(
68
+ NamiCampaignManagerEvents.ResultCampaign,
69
+ body => {
70
+ body.action = body.action.startsWith(searchString_Nami)
71
+ ? body.action.substring(5, body.action.length)
72
+ : body.action;
73
+
74
+ const {
75
+ action,
76
+ campaignId,
77
+ paywallId,
78
+ campaignName,
79
+ campaignType,
80
+ campaignLabel,
81
+ campaignUrl,
82
+ paywallName,
83
+ segmentId,
84
+ externalSegmentId,
85
+ deeplinkUrl,
86
+ skuId,
87
+ componentChangeId,
88
+ componentChangeName,
89
+ purchaseError,
90
+ purchases,
91
+ } = body;
92
+ if (actionCallback) {
93
+ actionCallback(
94
+ action,
95
+ campaignId,
96
+ paywallId,
97
+ campaignName,
98
+ campaignType,
99
+ campaignLabel,
100
+ campaignUrl,
101
+ paywallName,
102
+ segmentId,
103
+ externalSegmentId,
104
+ deeplinkUrl,
105
+ skuId,
106
+ componentChangeId,
107
+ componentChangeName,
108
+ purchaseError,
109
+ purchases,
110
+ );
111
+ }
112
+ },
113
+ );
114
+ RNNamiCampaignManager.launch(
115
+ label ?? null,
116
+ withUrl ?? null,
117
+ context ?? null,
118
+ resultCallback ?? (() => {}),
119
+ actionCallback ?? (() => {}),
120
+ );
121
+ },
122
+
123
+ isCampaignAvailable: campaignSource => {
124
+ return RNNamiCampaignManager.isCampaignAvailable(campaignSource ?? null);
125
+ },
126
+
127
+ registerAvailableCampaignsHandler(callback) {
128
+ if (typeof callback !== 'function') {
129
+ throw new Error('Expected callback to be a function.');
130
+ }
131
+ const subscription = this.emitter.addListener(
132
+ NamiCampaignManagerEvents.AvailableCampaignsChanged,
133
+ callback,
134
+ );
135
+
136
+ RNNamiCampaignManager.registerAvailableCampaignsHandler();
137
+ return () => {
138
+ if (subscription) {
139
+ subscription.remove();
140
+ }
141
+ };
142
+ },
143
+ };