Skip to content

Commit 9a5f1cb

Browse files
NickSxticlaude
andcommitted
Rework DeferredPurchasesListener to use native events
Replace JS-layer filtering with native DeferredPurchasesListener from Sandwich SDK. The listener now receives a DeferredTransaction object with full transaction details instead of entitlements. - Add DeferredTransaction model - Wire native onDeferredPurchaseCompleted event (iOS + Android) - Remove JS-side pendingPurchaseProductIds tracking - Update DeferredPurchasesListener to use DeferredTransaction - 10 tests passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dda31ef commit 9a5f1cb

9 files changed

Lines changed: 194 additions & 159 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,11 @@ 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)
305+
}
306+
302307
companion object {
303308
const val NAME = "RNQonversion"
304309
}

ios/RNQonversion.mm

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

320+
- (void)qonversionDidCompleteDeferredPurchase:(NSDictionary<NSString *,id> * _Nonnull)transaction {
321+
@try {
322+
[self emitOnDeferredPurchaseCompleted:transaction];
323+
} @catch (NSException *exception) {
324+
QNR_LOG_EXCEPTION("qonversionDidCompleteDeferredPurchase", exception);
325+
}
326+
}
327+
320328
- (void)shouldPurchasePromoProductWith:(NSString * _Nonnull)productId {
321329
@try {
322330
[self emitOnPromoPurchaseReceived:productId];

ios/RNQonversionImpl.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +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])
1617
}
1718

1819
class QonversionEventHandler: QonversionEventListener {
@@ -25,6 +26,10 @@ class QonversionEventHandler: QonversionEventListener {
2526
func qonversionDidReceiveUpdatedEntitlements(_ entitlements: [String: Any]) {
2627
delegate?.qonversionDidReceiveUpdatedEntitlements(entitlements)
2728
}
29+
30+
func qonversionDidCompleteDeferredPurchase(_ transaction: [String: Any]) {
31+
delegate?.qonversionDidCompleteDeferredPurchase(transaction)
32+
}
2833
}
2934

3035
@objc
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import Entitlement from './Entitlement';
1+
import DeferredTransaction from './DeferredTransaction';
22

33
export interface DeferredPurchasesListener {
44

55
/**
66
* Called when a deferred purchase completes.
77
* For example, when pending purchases like SCA, Ask to buy, etc., are approved and finalized.
8-
* @param entitlements the user's entitlements after the deferred purchase completion.
8+
* Provides full transaction details, including for consumable products without entitlements.
9+
* @param transaction the completed deferred transaction with full details.
910
*/
10-
onDeferredPurchaseCompleted(entitlements: Map<string, Entitlement>): void;
11+
onDeferredPurchaseCompleted(transaction: DeferredTransaction): void;
1112
}

src/dto/DeferredTransaction.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Represents a completed deferred purchase transaction with full details.
3+
*/
4+
export default class DeferredTransaction {
5+
/**
6+
* Store product identifier.
7+
*/
8+
productId: string;
9+
10+
/**
11+
* Store transaction identifier.
12+
*/
13+
transactionId: string | null;
14+
15+
/**
16+
* Original store transaction identifier.
17+
* For subscriptions, this is the ID of the first transaction.
18+
*/
19+
originalTransactionId: string | null;
20+
21+
/**
22+
* Type of the transaction: Subscription, Consumable, NonConsumable, or Unknown.
23+
*/
24+
type: string;
25+
26+
/**
27+
* Transaction value. May be 0 if unavailable.
28+
*/
29+
value: number;
30+
31+
/**
32+
* Currency code (e.g. "USD"). May be null if unavailable.
33+
*/
34+
currency: string | null;
35+
36+
constructor(
37+
productId: string,
38+
transactionId: string | null,
39+
originalTransactionId: string | null,
40+
type: string,
41+
value: number,
42+
currency: string | null
43+
) {
44+
this.productId = productId;
45+
this.transactionId = transactionId;
46+
this.originalTransactionId = originalTransactionId;
47+
this.type = type;
48+
this.value = value;
49+
this.currency = currency;
50+
}
51+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { default as QonversionConfigBuilder } from './QonversionConfigBuilder';
55

66
export type { EntitlementsUpdateListener } from './dto/EntitlementsUpdateListener';
77
export type { DeferredPurchasesListener } from './dto/DeferredPurchasesListener';
8+
export { default as DeferredTransaction } from './dto/DeferredTransaction';
89
export * from './dto/enums';
910
export { default as IntroEligibility } from './dto/IntroEligibility';
1011
export { default as Offering } from './dto/Offering';

src/internal/Mapper.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import QonversionError from '../dto/QonversionError';
5656
import NoCodesError from '../dto/NoCodesError';
5757
import PurchaseResult from '../dto/PurchaseResult';
5858
import StoreTransaction from '../dto/StoreTransaction';
59+
import DeferredTransaction from '../dto/DeferredTransaction';
5960

6061
export type QProduct = {
6162
id: string;
@@ -1250,6 +1251,27 @@ class Mapper {
12501251
}
12511252

12521253
// endregion
1254+
1255+
// region DeferredTransaction
1256+
1257+
static convertDeferredTransaction(
1258+
transaction: Record<string, any> | null | undefined
1259+
): DeferredTransaction | null {
1260+
if (!transaction) {
1261+
return null;
1262+
}
1263+
1264+
return new DeferredTransaction(
1265+
transaction.productId ?? '',
1266+
transaction.transactionId ?? null,
1267+
transaction.originalTransactionId ?? null,
1268+
transaction.type ?? 'Unknown',
1269+
transaction.value ?? 0,
1270+
transaction.currency ?? null
1271+
);
1272+
}
1273+
1274+
// endregion
12531275
}
12541276

12551277
export default Mapper;

src/internal/QonversionInternal.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ 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 pendingPurchaseProductIds: Set<string> = new Set();
3736
private entitlementsEventSubscribed = false;
37+
private deferredPurchaseEventSubscribed = false;
3838

3939
constructor(qonversionConfig: QonversionConfig) {
4040
RNQonversion.storeSDKInfo(sdkSource, sdkVersion);
@@ -126,10 +126,6 @@ export default class QonversionInternal implements QonversionApi {
126126
throw new Error("Failed to parse PurchaseResult");
127127
}
128128

129-
if (mappedResult.isPending) {
130-
this.pendingPurchaseProductIds.add(product.qonversionId);
131-
}
132-
133129
return mappedResult;
134130
}
135131

@@ -401,24 +397,25 @@ export default class QonversionInternal implements QonversionApi {
401397
}
402398
}
403399

400+
private subscribeToDeferredPurchaseEvent() {
401+
if (!this.deferredPurchaseEventSubscribed) {
402+
RNQonversion.onDeferredPurchaseCompleted(this.deferredPurchaseCompletedEventHandler);
403+
this.deferredPurchaseEventSubscribed = true;
404+
}
405+
}
406+
404407
private entitlementsUpdatedEventHandler = (payload: Object) => {
405408
const entitlements = Mapper.convertEntitlements(payload as Record<string, QEntitlement>);
406409

407410
this.entitlementsUpdateListener?.onEntitlementsUpdated(entitlements);
408-
409-
if (this.deferredPurchasesListener && this.hasPendingPurchaseCompleted(entitlements)) {
410-
this.deferredPurchasesListener.onDeferredPurchaseCompleted(entitlements);
411-
}
412411
}
413412

414-
private hasPendingPurchaseCompleted(entitlements: Map<string, Entitlement>): boolean {
415-
for (const [, entitlement] of entitlements) {
416-
if (entitlement.isActive && this.pendingPurchaseProductIds.has(entitlement.productId)) {
417-
this.pendingPurchaseProductIds.delete(entitlement.productId);
418-
return true;
419-
}
413+
private deferredPurchaseCompletedEventHandler = (payload: Object) => {
414+
const transaction = Mapper.convertDeferredTransaction(payload as Record<string, any>);
415+
416+
if (transaction) {
417+
this.deferredPurchasesListener?.onDeferredPurchaseCompleted(transaction);
420418
}
421-
return false;
422419
}
423420

424421
private promoPurchaseReceivedEventHandler = (productId: string) => {
@@ -436,7 +433,7 @@ export default class QonversionInternal implements QonversionApi {
436433
}
437434

438435
setDeferredPurchasesListener(listener: DeferredPurchasesListener) {
439-
this.subscribeToEntitlementsEvent();
436+
this.subscribeToDeferredPurchaseEvent();
440437
this.deferredPurchasesListener = listener;
441438
}
442439

0 commit comments

Comments
 (0)