Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ class QonversionModule(reactContext: ReactApplicationContext) : NativeQonversion
emitOnEntitlementsUpdated(mappedEntitlements)
}

override fun onDeferredPurchaseCompleted(transaction: BridgeData) {
val mappedTransaction = EntitiesConverter.convertMapToWritableMap(transaction)
emitOnDeferredPurchaseCompleted(mappedTransaction)
}

companion object {
const val NAME = "RNQonversion"
}
Expand Down
8 changes: 8 additions & 0 deletions ios/RNQonversion.mm
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,14 @@ - (void)qonversionDidReceiveUpdatedEntitlements:(NSDictionary<NSString *,id> * _
}
}

- (void)qonversionDidCompleteDeferredPurchase:(NSDictionary<NSString *,id> * _Nonnull)transaction {
@try {
[self emitOnDeferredPurchaseCompleted:transaction];
} @catch (NSException *exception) {
QNR_LOG_EXCEPTION("qonversionDidCompleteDeferredPurchase", exception);
}
}

- (void)shouldPurchasePromoProductWith:(NSString * _Nonnull)productId {
@try {
[self emitOnPromoPurchaseReceived:productId];
Expand Down
5 changes: 5 additions & 0 deletions ios/RNQonversionImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import React
public protocol QonversionEventDelegate {
func shouldPurchasePromoProduct(with productId: String)
func qonversionDidReceiveUpdatedEntitlements(_ entitlements: [String: Any])
func qonversionDidCompleteDeferredPurchase(_ transaction: [String: Any])
}

class QonversionEventHandler: QonversionEventListener {
Expand All @@ -25,6 +26,10 @@ class QonversionEventHandler: QonversionEventListener {
func qonversionDidReceiveUpdatedEntitlements(_ entitlements: [String: Any]) {
delegate?.qonversionDidReceiveUpdatedEntitlements(entitlements)
}

func qonversionDidCompleteDeferredPurchase(_ transaction: [String: Any]) {
delegate?.qonversionDidCompleteDeferredPurchase(transaction)
}
}

@objc
Expand Down
20 changes: 20 additions & 0 deletions src/QonversionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Offerings from './dto/Offerings';
import IntroEligibility from './dto/IntroEligibility';
import User from './dto/User';
import type {EntitlementsUpdateListener} from './dto/EntitlementsUpdateListener';
import type {DeferredPurchasesListener} from './dto/DeferredPurchasesListener';
import type {PromoPurchasesListener} from './dto/PromoPurchasesListener';
import RemoteConfig from "./dto/RemoteConfig";
import RemoteConfigList from "./dto/RemoteConfigList";
Expand Down Expand Up @@ -239,9 +240,28 @@ export interface QonversionApi {
* with {@link Qonversion.initialize}.
*
* @param listener listener to be called when entitlements update
* @deprecated Use {@link setDeferredPurchasesListener} instead.
*/
setEntitlementsUpdateListener(listener: EntitlementsUpdateListener): void;

/**
* Provide a listener to be notified about deferred purchase completions.
*
* Deferred purchases happen when transactions require additional steps to complete,
* such as SCA (Strong Customer Authentication), Ask to Buy, or other pending transactions.
* This listener will be called when such purchases are finalized.
*
* Make sure you provide this listener for being up-to-date with deferred purchase completions.
* Also, please, consider that this listener should live for the whole lifetime of the application.
*
* You may set deferred purchases listener both *after* Qonversion SDK initializing
* with {@link QonversionApi.setDeferredPurchasesListener} and *while* Qonversion initializing
* with {@link Qonversion.initialize}.
*
* @param listener listener to be called when a deferred purchase completes
*/
setDeferredPurchasesListener(listener: DeferredPurchasesListener): void;

/**
* iOS only. Does nothing if called on Android.
*
Expand Down
9 changes: 7 additions & 2 deletions src/QonversionConfig.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import {EntitlementsCacheLifetime, Environment, LaunchMode} from './dto/enums';
import type {EntitlementsUpdateListener} from './dto/EntitlementsUpdateListener';
import type {DeferredPurchasesListener} from './dto/DeferredPurchasesListener';

class QonversionConfig {
readonly projectKey: string;
readonly launchMode: LaunchMode;
readonly environment: Environment;
readonly entitlementsCacheLifetime: EntitlementsCacheLifetime;
/** @deprecated Use {@link deferredPurchasesListener} instead. */
readonly entitlementsUpdateListener: EntitlementsUpdateListener | undefined;
readonly proxyUrl: string | undefined;
readonly kidsMode: boolean;
readonly deferredPurchasesListener: DeferredPurchasesListener | undefined;

constructor(
projectKey: string,
launchMode: LaunchMode,
environment: Environment = Environment.PRODUCTION,
entitlementsCacheLifetime: EntitlementsCacheLifetime = EntitlementsCacheLifetime.MONTH,
entitlementsUpdateListener: EntitlementsUpdateListener | undefined = undefined,
proxyUrl: string | undefined,
kidsMode: boolean = false
proxyUrl: string | undefined = undefined,
kidsMode: boolean = false,
deferredPurchasesListener: DeferredPurchasesListener | undefined = undefined,
) {
this.projectKey = projectKey;
this.launchMode = launchMode;
Expand All @@ -26,6 +30,7 @@ class QonversionConfig {
this.entitlementsUpdateListener = entitlementsUpdateListener;
this.proxyUrl = proxyUrl;
this.kidsMode = kidsMode;
this.deferredPurchasesListener = deferredPurchasesListener;
}
}

Expand Down
24 changes: 23 additions & 1 deletion src/QonversionConfigBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {EntitlementsCacheLifetime, Environment, LaunchMode} from './dto/enums';
import type {EntitlementsUpdateListener} from './dto/EntitlementsUpdateListener';
import type {DeferredPurchasesListener} from './dto/DeferredPurchasesListener';
import QonversionConfig from './QonversionConfig';

class QonversionConfigBuilder {
Expand All @@ -14,6 +15,7 @@ class QonversionConfigBuilder {
private environment: Environment = Environment.PRODUCTION;
private entitlementsCacheLifetime: EntitlementsCacheLifetime = EntitlementsCacheLifetime.MONTH;
private entitlementsUpdateListener: EntitlementsUpdateListener | undefined = undefined;
private deferredPurchasesListener: DeferredPurchasesListener | undefined = undefined;
private proxyUrl: string | undefined = undefined;
private kidsMode: boolean = false;

Expand Down Expand Up @@ -51,12 +53,31 @@ class QonversionConfigBuilder {
*
* @param entitlementsUpdateListener listener to be called when entitlements update.
* @return builder instance for chain calls.
* @deprecated Use {@link setDeferredPurchasesListener} instead.
*/
setEntitlementsUpdateListener(entitlementsUpdateListener: EntitlementsUpdateListener): QonversionConfigBuilder {
this.entitlementsUpdateListener = entitlementsUpdateListener;
return this;
}

/**
* Provide a listener to be notified about deferred purchase completions.
*
* Deferred purchases happen when transactions require additional steps to complete,
* such as SCA (Strong Customer Authentication), Ask to Buy, or other pending transactions.
* This listener will be called when such purchases are finalized.
*
* Make sure you provide this listener for being up-to-date with deferred purchase completions.
* Also, please, consider that this listener should live for the whole lifetime of the application.
*
* @param listener listener to be called when a deferred purchase completes.
* @return builder instance for chain calls.
*/
setDeferredPurchasesListener(listener: DeferredPurchasesListener): QonversionConfigBuilder {
this.deferredPurchasesListener = listener;
return this;
}

/**
* Provide a URL to your proxy server which will redirect all the requests from the app
* to our API. Please, contact us before using this feature.
Expand Down Expand Up @@ -94,7 +115,8 @@ class QonversionConfigBuilder {
this.entitlementsCacheLifetime,
this.entitlementsUpdateListener,
this.proxyUrl,
this.kidsMode
this.kidsMode,
this.deferredPurchasesListener,
)
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/dto/DeferredPurchasesListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import DeferredTransaction from './DeferredTransaction';

export interface DeferredPurchasesListener {

/**
* Called when a deferred purchase completes.
* For example, when pending purchases like SCA, Ask to buy, etc., are approved and finalized.
* Provides full transaction details, including for consumable products without entitlements.
* @param transaction the completed deferred transaction with full details.
*/
onDeferredPurchaseCompleted(transaction: DeferredTransaction): void;
}
51 changes: 51 additions & 0 deletions src/dto/DeferredTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Represents a completed deferred purchase transaction with full details.
*/
export default class DeferredTransaction {
/**
* Store product identifier.
*/
productId: string;

/**
* Store transaction identifier.
*/
transactionId: string | null;

/**
* Original store transaction identifier.
* For subscriptions, this is the ID of the first transaction.
*/
originalTransactionId: string | null;

/**
* Type of the transaction: Subscription, Consumable, NonConsumable, or Unknown.
*/
type: string;

/**
* Transaction value. May be 0 if unavailable.
*/
value: number;

/**
* Currency code (e.g. "USD"). May be null if unavailable.
*/
currency: string | null;

constructor(
productId: string,
transactionId: string | null,
originalTransactionId: string | null,
type: string,
value: number,
currency: string | null
) {
this.productId = productId;
this.transactionId = transactionId;
this.originalTransactionId = originalTransactionId;
this.type = type;
this.value = value;
this.currency = currency;
}
}
3 changes: 3 additions & 0 deletions src/dto/EntitlementsUpdateListener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Entitlement from './Entitlement';

/**
* @deprecated Use {@link DeferredPurchasesListener} instead.
*/
export interface EntitlementsUpdateListener {

/**
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export { default as QonversionConfig } from './QonversionConfig';
export { default as QonversionConfigBuilder } from './QonversionConfigBuilder';

export type { EntitlementsUpdateListener } from './dto/EntitlementsUpdateListener';
export type { DeferredPurchasesListener } from './dto/DeferredPurchasesListener';
export { default as DeferredTransaction } from './dto/DeferredTransaction';
export * from './dto/enums';
export { default as IntroEligibility } from './dto/IntroEligibility';
export { default as Offering } from './dto/Offering';
Expand Down
22 changes: 22 additions & 0 deletions src/internal/Mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import QonversionError from '../dto/QonversionError';
import NoCodesError from '../dto/NoCodesError';
import PurchaseResult from '../dto/PurchaseResult';
import StoreTransaction from '../dto/StoreTransaction';
import DeferredTransaction from '../dto/DeferredTransaction';

export type QProduct = {
id: string;
Expand Down Expand Up @@ -1250,6 +1251,27 @@ class Mapper {
}

// endregion

// region DeferredTransaction

static convertDeferredTransaction(
transaction: Record<string, any> | null | undefined
): DeferredTransaction | null {
if (!transaction) {
return null;
}

return new DeferredTransaction(
transaction.productId ?? '',
transaction.transactionId ?? null,
transaction.originalTransactionId ?? null,
transaction.type ?? 'Unknown',
transaction.value ?? 0,
transaction.currency ?? null
);
}

// endregion
}

export default Mapper;
41 changes: 37 additions & 4 deletions src/internal/QonversionInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Product from "../dto/Product";
import PurchaseResult from "../dto/PurchaseResult";
import {isAndroid, isIos} from "./utils";
import type {EntitlementsUpdateListener} from '../dto/EntitlementsUpdateListener';
import type {DeferredPurchasesListener} from '../dto/DeferredPurchasesListener';
import type {PromoPurchasesListener} from '../dto/PromoPurchasesListener';
import User from '../dto/User';
import PurchaseOptions from '../dto/PurchaseOptions';
Expand All @@ -30,7 +31,10 @@ export const sdkSource = "rn";
export default class QonversionInternal implements QonversionApi {

private entitlementsUpdateListener: EntitlementsUpdateListener | null = null;
private deferredPurchasesListener: DeferredPurchasesListener | null = null;
private promoPurchasesDelegate: PromoPurchasesListener | null = null;
private entitlementsEventSubscribed = false;
private deferredPurchaseEventSubscribed = false;

constructor(qonversionConfig: QonversionConfig) {
RNQonversion.storeSDKInfo(sdkSource, sdkVersion);
Expand All @@ -46,6 +50,10 @@ export default class QonversionInternal implements QonversionApi {
if (qonversionConfig.entitlementsUpdateListener) {
this.setEntitlementsUpdateListener(qonversionConfig.entitlementsUpdateListener);
}

if (qonversionConfig.deferredPurchasesListener) {
this.setDeferredPurchasesListener(qonversionConfig.deferredPurchasesListener);
}
}

syncHistoricalData () {
Expand Down Expand Up @@ -382,11 +390,34 @@ export default class QonversionInternal implements QonversionApi {
return;
}

private subscribeToEntitlementsEvent() {
if (!this.entitlementsEventSubscribed) {
RNQonversion.onEntitlementsUpdated(this.entitlementsUpdatedEventHandler);
this.entitlementsEventSubscribed = true;
}
}

private subscribeToDeferredPurchaseEvent() {
if (!this.deferredPurchaseEventSubscribed) {
RNQonversion.onDeferredPurchaseCompleted(this.deferredPurchaseCompletedEventHandler);
this.deferredPurchaseEventSubscribed = true;
}
}

private entitlementsUpdatedEventHandler = (payload: Object) => {
const entitlements = Mapper.convertEntitlements(payload as Record<string, QEntitlement>);

this.entitlementsUpdateListener?.onEntitlementsUpdated(entitlements);
}

private deferredPurchaseCompletedEventHandler = (payload: Object) => {
const transaction = Mapper.convertDeferredTransaction(payload as Record<string, any>);

if (transaction) {
this.deferredPurchasesListener?.onDeferredPurchaseCompleted(transaction);
}
}

private promoPurchaseReceivedEventHandler = (productId: string) => {
const promoPurchaseExecutor = async () => {
const entitlements = await RNQonversion.promoPurchase(productId);
Expand All @@ -397,13 +428,15 @@ export default class QonversionInternal implements QonversionApi {
}

setEntitlementsUpdateListener(listener: EntitlementsUpdateListener) {
if (this.entitlementsUpdateListener == null) {
RNQonversion.onEntitlementsUpdated(this.entitlementsUpdatedEventHandler);
}

this.subscribeToEntitlementsEvent();
this.entitlementsUpdateListener = listener;
}

setDeferredPurchasesListener(listener: DeferredPurchasesListener) {
this.subscribeToDeferredPurchaseEvent();
this.deferredPurchasesListener = listener;
}

setPromoPurchasesDelegate(delegate: PromoPurchasesListener) {
if (!isIos()) {
return;
Expand Down
Loading
Loading