Skip to content

Commit 801d40d

Browse files
NickSxticlaude
andcommitted
Address review: route EntitlementsUpdateListener through onDeferredPurchaseCompleted
- Rename `transaction` -> `purchaseResult` in Android and iOS bridges - Remove extra `// endregion` in Mapper.ts - Reorder `deferredPurchasesListener` after `entitlementsUpdateListener` in config and builder - Route deprecated EntitlementsUpdateListener through onDeferredPurchaseCompleted (same pattern as native SDKs), eliminating logic duplication Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 44fa617 commit 801d40d

File tree

8 files changed

+63
-66
lines changed

8 files changed

+63
-66
lines changed

android/src/main/java/com/qonversion/reactnativesdk/QonversionModule.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,9 @@ class QonversionModule(reactContext: ReactApplicationContext) : NativeQonversion
299299
emitOnEntitlementsUpdated(mappedEntitlements)
300300
}
301301

302-
override fun onDeferredPurchaseCompleted(transaction: BridgeData) {
303-
val mappedTransaction = EntitiesConverter.convertMapToWritableMap(transaction)
304-
emitOnDeferredPurchaseCompleted(mappedTransaction)
302+
override fun onDeferredPurchaseCompleted(purchaseResult: BridgeData) {
303+
val mappedPurchaseResult = EntitiesConverter.convertMapToWritableMap(purchaseResult)
304+
emitOnDeferredPurchaseCompleted(mappedPurchaseResult)
305305
}
306306

307307
companion object {

ios/RNQonversion.mm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,9 @@ - (void)qonversionDidReceiveUpdatedEntitlements:(NSDictionary<NSString *,id> * _
317317
}
318318
}
319319

320-
- (void)qonversionDidCompleteDeferredPurchase:(NSDictionary<NSString *,id> * _Nonnull)transaction {
320+
- (void)qonversionDidCompleteDeferredPurchase:(NSDictionary<NSString *,id> * _Nonnull)purchaseResult {
321321
@try {
322-
[self emitOnDeferredPurchaseCompleted:transaction];
322+
[self emitOnDeferredPurchaseCompleted:purchaseResult];
323323
} @catch (NSException *exception) {
324324
QNR_LOG_EXCEPTION("qonversionDidCompleteDeferredPurchase", exception);
325325
}

ios/RNQonversionImpl.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import React
1313
public protocol QonversionEventDelegate {
1414
func shouldPurchasePromoProduct(with productId: String)
1515
func qonversionDidReceiveUpdatedEntitlements(_ entitlements: [String: Any])
16-
func qonversionDidCompleteDeferredPurchase(_ transaction: [String: Any])
16+
func qonversionDidCompleteDeferredPurchase(_ purchaseResult: [String: Any])
1717
}
1818

1919
class QonversionEventHandler: QonversionEventListener {
@@ -27,8 +27,8 @@ class QonversionEventHandler: QonversionEventListener {
2727
delegate?.qonversionDidReceiveUpdatedEntitlements(entitlements)
2828
}
2929

30-
func qonversionDidCompleteDeferredPurchase(_ transaction: [String: Any]) {
31-
delegate?.qonversionDidCompleteDeferredPurchase(transaction)
30+
func qonversionDidCompleteDeferredPurchase(_ purchaseResult: [String: Any]) {
31+
delegate?.qonversionDidCompleteDeferredPurchase(purchaseResult)
3232
}
3333
}
3434

src/QonversionConfig.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,28 @@ class QonversionConfig {
99
readonly entitlementsCacheLifetime: EntitlementsCacheLifetime;
1010
/** @deprecated Use {@link deferredPurchasesListener} instead. */
1111
readonly entitlementsUpdateListener: EntitlementsUpdateListener | undefined;
12+
readonly deferredPurchasesListener: DeferredPurchasesListener | undefined;
1213
readonly proxyUrl: string | undefined;
1314
readonly kidsMode: boolean;
14-
readonly deferredPurchasesListener: DeferredPurchasesListener | undefined;
1515

1616
constructor(
1717
projectKey: string,
1818
launchMode: LaunchMode,
1919
environment: Environment = Environment.PRODUCTION,
2020
entitlementsCacheLifetime: EntitlementsCacheLifetime = EntitlementsCacheLifetime.MONTH,
2121
entitlementsUpdateListener: EntitlementsUpdateListener | undefined = undefined,
22+
deferredPurchasesListener: DeferredPurchasesListener | undefined = undefined,
2223
proxyUrl: string | undefined = undefined,
2324
kidsMode: boolean = false,
24-
deferredPurchasesListener: DeferredPurchasesListener | undefined = undefined,
2525
) {
2626
this.projectKey = projectKey;
2727
this.launchMode = launchMode;
2828
this.environment = environment;
2929
this.entitlementsCacheLifetime = entitlementsCacheLifetime;
3030
this.entitlementsUpdateListener = entitlementsUpdateListener;
31+
this.deferredPurchasesListener = deferredPurchasesListener;
3132
this.proxyUrl = proxyUrl;
3233
this.kidsMode = kidsMode;
33-
this.deferredPurchasesListener = deferredPurchasesListener;
3434
}
3535
}
3636

src/QonversionConfigBuilder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class QonversionConfigBuilder {
1919
private proxyUrl: string | undefined = undefined;
2020
private kidsMode: boolean = false;
2121

22+
2223
/**
2324
* Set current application {@link Environment}. Used to distinguish sandbox and production users.
2425
*
@@ -114,9 +115,9 @@ class QonversionConfigBuilder {
114115
this.environment,
115116
this.entitlementsCacheLifetime,
116117
this.entitlementsUpdateListener,
118+
this.deferredPurchasesListener,
117119
this.proxyUrl,
118120
this.kidsMode,
119-
this.deferredPurchasesListener,
120121
)
121122
}
122123
}

src/internal/Mapper.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,8 +1250,6 @@ class Mapper {
12501250
}
12511251

12521252
// endregion
1253-
1254-
// endregion
12551253
}
12561254

12571255
export default Mapper;

src/internal/QonversionInternal.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export default class QonversionInternal implements QonversionApi {
3333
private entitlementsUpdateListener: EntitlementsUpdateListener | null = null;
3434
private deferredPurchasesListener: DeferredPurchasesListener | null = null;
3535
private promoPurchasesDelegate: PromoPurchasesListener | null = null;
36-
private entitlementsEventSubscribed = false;
3736
private deferredPurchaseEventSubscribed = false;
3837

3938
constructor(qonversionConfig: QonversionConfig) {
@@ -390,31 +389,22 @@ export default class QonversionInternal implements QonversionApi {
390389
return;
391390
}
392391

393-
private subscribeToEntitlementsEvent() {
394-
if (!this.entitlementsEventSubscribed) {
395-
RNQonversion.onEntitlementsUpdated(this.entitlementsUpdatedEventHandler);
396-
this.entitlementsEventSubscribed = true;
397-
}
398-
}
399-
400392
private subscribeToDeferredPurchaseEvent() {
401393
if (!this.deferredPurchaseEventSubscribed) {
402394
RNQonversion.onDeferredPurchaseCompleted(this.deferredPurchaseCompletedEventHandler);
403395
this.deferredPurchaseEventSubscribed = true;
404396
}
405397
}
406398

407-
private entitlementsUpdatedEventHandler = (payload: Object) => {
408-
const entitlements = Mapper.convertEntitlements(payload as Record<string, QEntitlement>);
409-
410-
this.entitlementsUpdateListener?.onEntitlementsUpdated(entitlements);
411-
}
412-
413399
private deferredPurchaseCompletedEventHandler = (payload: Object) => {
414400
const purchaseResult = Mapper.convertPurchaseResult(payload as Record<string, any>);
415401

416402
if (purchaseResult) {
417403
this.deferredPurchasesListener?.onDeferredPurchaseCompleted(purchaseResult);
404+
405+
if (purchaseResult.entitlements) {
406+
this.entitlementsUpdateListener?.onEntitlementsUpdated(purchaseResult.entitlements);
407+
}
418408
}
419409
}
420410

@@ -428,7 +418,7 @@ export default class QonversionInternal implements QonversionApi {
428418
}
429419

430420
setEntitlementsUpdateListener(listener: EntitlementsUpdateListener) {
431-
this.subscribeToEntitlementsEvent();
421+
this.subscribeToDeferredPurchaseEvent();
432422
this.entitlementsUpdateListener = listener;
433423
}
434424

src/internal/__tests__/QonversionInternal.test.ts

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { EntitlementsUpdateListener } from '../../dto/EntitlementsUpdateListener';
22
import type { DeferredPurchasesListener } from '../../dto/DeferredPurchasesListener';
3-
import type { QEntitlement } from '../Mapper';
43
import PurchaseResult from '../../dto/PurchaseResult';
4+
import Entitlement from '../../dto/Entitlement';
55
import { PurchaseResultStatus, PurchaseResultSource } from '../../dto/enums';
66

77
// Capture event handlers registered on the native module mock
@@ -20,9 +20,6 @@ jest.mock('../specs/NativeQonversionModule', () => ({
2020
default: {
2121
storeSDKInfo: jest.fn(),
2222
initializeSdk: jest.fn(),
23-
onEntitlementsUpdated: jest.fn((handler: Function) => {
24-
eventHandlers['onEntitlementsUpdated'] = handler;
25-
}),
2623
onDeferredPurchaseCompleted: jest.fn((handler: Function) => {
2724
eventHandlers['onDeferredPurchaseCompleted'] = handler;
2825
}),
@@ -31,7 +28,19 @@ jest.mock('../specs/NativeQonversionModule', () => ({
3128
},
3229
}));
3330

31+
const mockEntitlements = new Map<string, Entitlement>();
32+
mockEntitlements.set('premium', { id: 'premium', productId: 'premium_product', isActive: true } as unknown as Entitlement);
33+
3434
const mockPurchaseResult = new PurchaseResult(
35+
PurchaseResultStatus.SUCCESS,
36+
mockEntitlements,
37+
null,
38+
false,
39+
PurchaseResultSource.API,
40+
null
41+
);
42+
43+
const mockPurchaseResultNoEntitlements = new PurchaseResult(
3544
PurchaseResultStatus.SUCCESS,
3645
null,
3746
null,
@@ -60,6 +69,7 @@ import QonversionInternal from '../QonversionInternal';
6069
import QonversionConfig from '../../QonversionConfig';
6170
import { Environment, EntitlementsCacheLifetime, LaunchMode } from '../../dto/enums';
6271
import RNQonversion from '../specs/NativeQonversionModule';
72+
import Mapper from '../Mapper';
6373

6474
function createConfig() {
6575
return new QonversionConfig(
@@ -69,24 +79,21 @@ function createConfig() {
6979
EntitlementsCacheLifetime.MONTH,
7080
undefined,
7181
undefined,
82+
undefined,
7283
false,
7384
);
7485
}
7586

7687
const samplePurchaseResult = {
7788
status: 'Success',
78-
entitlements: null,
89+
entitlements: { premium: { id: 'premium', productId: 'premium_product', isActive: true } },
7990
error: null,
8091
isFallbackGenerated: false,
8192
source: 'Api',
8293
storeTransaction: null,
8394
};
8495

85-
const entitlementsPayload: Record<string, QEntitlement> = {
86-
premium: { id: 'premium', productId: 'premium_product', isActive: true } as unknown as QEntitlement,
87-
};
88-
89-
describe('QonversionInternal - DeferredPurchasesListener (native event)', () => {
96+
describe('QonversionInternal - DeferredPurchasesListener', () => {
9097
beforeEach(() => {
9198
jest.clearAllMocks();
9299
for (const key of Object.keys(eventHandlers)) {
@@ -149,15 +156,6 @@ describe('QonversionInternal - DeferredPurchasesListener (native event)', () =>
149156

150157
expect(RNQonversion.onDeferredPurchaseCompleted).toHaveBeenCalledTimes(1);
151158
});
152-
153-
it('deferred listener does NOT subscribe to onEntitlementsUpdated', () => {
154-
const instance = new QonversionInternal(createConfig());
155-
const listener: DeferredPurchasesListener = { onDeferredPurchaseCompleted: jest.fn() };
156-
157-
instance.setDeferredPurchasesListener(listener);
158-
159-
expect(RNQonversion.onEntitlementsUpdated).not.toHaveBeenCalled();
160-
});
161159
});
162160

163161
describe('QonversionInternal - deprecated setEntitlementsUpdateListener', () => {
@@ -168,23 +166,36 @@ describe('QonversionInternal - deprecated setEntitlementsUpdateListener', () =>
168166
}
169167
});
170168

171-
it('subscribes to onEntitlementsUpdated', () => {
169+
it('subscribes to onDeferredPurchaseCompleted (same as native SDK)', () => {
172170
const instance = new QonversionInternal(createConfig());
173171
const oldListener: EntitlementsUpdateListener = { onEntitlementsUpdated: jest.fn() };
174172

175173
instance.setEntitlementsUpdateListener(oldListener);
176174

177-
expect(RNQonversion.onEntitlementsUpdated).toHaveBeenCalled();
175+
expect(RNQonversion.onDeferredPurchaseCompleted).toHaveBeenCalled();
178176
});
179177

180-
it('fires for ALL entitlement updates', () => {
178+
it('fires with entitlements extracted from PurchaseResult', () => {
181179
const instance = new QonversionInternal(createConfig());
182180
const oldListener: EntitlementsUpdateListener = { onEntitlementsUpdated: jest.fn() };
183181

184182
instance.setEntitlementsUpdateListener(oldListener);
185-
fireEvent('onEntitlementsUpdated', entitlementsPayload);
183+
fireEvent('onDeferredPurchaseCompleted', samplePurchaseResult);
186184

187185
expect(oldListener.onEntitlementsUpdated).toHaveBeenCalledTimes(1);
186+
expect(oldListener.onEntitlementsUpdated).toHaveBeenCalledWith(mockEntitlements);
187+
});
188+
189+
it('does not fire when PurchaseResult has no entitlements', () => {
190+
(Mapper.convertPurchaseResult as jest.Mock).mockReturnValueOnce(mockPurchaseResultNoEntitlements);
191+
192+
const instance = new QonversionInternal(createConfig());
193+
const oldListener: EntitlementsUpdateListener = { onEntitlementsUpdated: jest.fn() };
194+
195+
instance.setEntitlementsUpdateListener(oldListener);
196+
fireEvent('onDeferredPurchaseCompleted', samplePurchaseResult);
197+
198+
expect(oldListener.onEntitlementsUpdated).not.toHaveBeenCalled();
188199
});
189200
});
190201

@@ -196,7 +207,7 @@ describe('QonversionInternal - both listeners coexist', () => {
196207
}
197208
});
198209

199-
it('both listeners fire independently from their own native events', () => {
210+
it('both listeners fire from onDeferredPurchaseCompleted', () => {
200211
const oldListener: EntitlementsUpdateListener = { onEntitlementsUpdated: jest.fn() };
201212
const newListener: DeferredPurchasesListener = { onDeferredPurchaseCompleted: jest.fn() };
202213

@@ -207,24 +218,21 @@ describe('QonversionInternal - both listeners coexist', () => {
207218
EntitlementsCacheLifetime.MONTH,
208219
oldListener,
209220
undefined,
221+
undefined,
210222
false,
211-
newListener,
212223
);
213224

214-
void new QonversionInternal(config);
225+
const instance = new QonversionInternal(config);
226+
instance.setDeferredPurchasesListener(newListener);
215227

216-
// Entitlements update fires old listener only
217-
fireEvent('onEntitlementsUpdated', entitlementsPayload);
218-
expect(oldListener.onEntitlementsUpdated).toHaveBeenCalledTimes(1);
219-
expect(newListener.onDeferredPurchaseCompleted).not.toHaveBeenCalled();
220-
221-
// Deferred purchase fires new listener only
222228
fireEvent('onDeferredPurchaseCompleted', samplePurchaseResult);
223-
expect(oldListener.onEntitlementsUpdated).toHaveBeenCalledTimes(1); // still 1
229+
224230
expect(newListener.onDeferredPurchaseCompleted).toHaveBeenCalledTimes(1);
231+
expect(oldListener.onEntitlementsUpdated).toHaveBeenCalledTimes(1);
232+
expect(oldListener.onEntitlementsUpdated).toHaveBeenCalledWith(mockEntitlements);
225233
});
226234

227-
it('subscribes to both native events independently', () => {
235+
it('subscribes to onDeferredPurchaseCompleted only once for both listeners', () => {
228236
const oldListener: EntitlementsUpdateListener = { onEntitlementsUpdated: jest.fn() };
229237
const newListener: DeferredPurchasesListener = { onDeferredPurchaseCompleted: jest.fn() };
230238

@@ -235,13 +243,13 @@ describe('QonversionInternal - both listeners coexist', () => {
235243
EntitlementsCacheLifetime.MONTH,
236244
oldListener,
237245
undefined,
246+
undefined,
238247
false,
239-
newListener,
240248
);
241249

242-
void new QonversionInternal(config);
250+
const instance = new QonversionInternal(config);
251+
instance.setDeferredPurchasesListener(newListener);
243252

244-
expect(RNQonversion.onEntitlementsUpdated).toHaveBeenCalledTimes(1);
245253
expect(RNQonversion.onDeferredPurchaseCompleted).toHaveBeenCalledTimes(1);
246254
});
247255
});

0 commit comments

Comments
 (0)