Skip to content
Merged
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 @@ -89,6 +89,33 @@ class QonversionModule(reactContext: ReactApplicationContext) : NativeQonversion
)
}

@ReactMethod
override fun purchaseWithResult(
productId: String,
quantity: Double,
contextKeys: ReadableArray?,
promoOffer: ReadableMap?,
offerId: String?,
applyOffer: Boolean,
oldProductId: String?,
updatePolicyKey: String?,
promise: Promise
) {
var contextKeysList: List<String>? = null
if (contextKeys != null) {
contextKeysList = EntitiesConverter.convertArrayToStringList(contextKeys)
}
qonversionSandwich.purchaseWithResult(
productId,
offerId,
applyOffer,
oldProductId,
updatePolicyKey,
contextKeysList,
getResultListener(promise)
)
}

@ReactMethod
override fun updatePurchase(
productId: String,
Expand Down
19 changes: 12 additions & 7 deletions example/src/screens/NoCodesScreen/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,23 @@ const NoCodesScreen: React.FC = () => {
const purchaseDelegate: PurchaseDelegate = {
purchase: async (product: Product) => {
console.log('🔄 [PurchaseDelegate] Starting purchase for product:', product.qonversionId);
try {
const entitlements = await Qonversion.getSharedInstance().purchaseProduct(product);
console.log('✅ [PurchaseDelegate] Purchase successful:', Object.fromEntries(entitlements));
const result = await Qonversion.getSharedInstance().purchaseWithResult(product);
console.log('✅ [PurchaseDelegate] Purchase result:', result.status);

if (result.isSuccess) {
dispatch({ type: 'ADD_NOCODES_EVENT', payload: `Purchase completed: ${product.qonversionId}` });
Snackbar.show({
text: `Purchase completed: ${product.qonversionId}`,
duration: Snackbar.LENGTH_SHORT,
});
} catch (error: any) {
console.error('❌ [PurchaseDelegate] Purchase failed:', error);
dispatch({ type: 'ADD_NOCODES_EVENT', payload: `Purchase failed: ${error.message}` });
throw error; // Re-throw to let NoCodes SDK handle the error
} else if (result.isCanceled) {
dispatch({ type: 'ADD_NOCODES_EVENT', payload: `Purchase canceled: ${product.qonversionId}` });
} else if (result.isPending) {
dispatch({ type: 'ADD_NOCODES_EVENT', payload: `Purchase pending: ${product.qonversionId}` });
} else if (result.isError) {
console.error('❌ [PurchaseDelegate] Purchase failed:', result.error);
dispatch({ type: 'ADD_NOCODES_EVENT', payload: `Purchase failed: ${result.error?.description}` });
throw new Error(result.error?.description || 'Purchase failed');
}
},
restore: async () => {
Expand Down
50 changes: 36 additions & 14 deletions example/src/screens/ProductDetailScreen/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,48 @@ const ProductDetailScreen: React.FC<ProductDetailScreenProps> = ({
const purchaseProduct = async () => {
try {
console.log(
'🔄 [Qonversion] Starting purchaseProduct() call with product:',
'🔄 [Qonversion] Starting purchaseWithResult() call with product:',
product
);
dispatch({ type: 'SET_LOADING', payload: true });
const entitlements =
await Qonversion.getSharedInstance().purchaseProduct(product);
const result =
await Qonversion.getSharedInstance().purchaseWithResult(product);
console.log(
'✅ [Qonversion] purchaseProduct() call successful:',
Object.fromEntries(entitlements)
'✅ [Qonversion] purchaseWithResult() call completed with status:',
result.status
);
dispatch({ type: 'SET_ENTITLEMENTS', payload: entitlements });
Snackbar.show({
text: 'Product purchased successfully!',
duration: Snackbar.LENGTH_SHORT,
});
} catch (error: any) {
console.error('❌ [Qonversion] purchaseProduct() call failed:', error);
if (!error.userCanceled) {
Alert.alert('Error', error.message);

console.log('📦 [Qonversion] Purchase result:', result);

if (result.isSuccess) {
console.log('✅ [Qonversion] Purchase successful!');
if (result.entitlements) {
dispatch({ type: 'SET_ENTITLEMENTS', payload: result.entitlements });
}
Snackbar.show({
text: 'Product purchased successfully!',
duration: Snackbar.LENGTH_SHORT,
});
} else if (result.isCanceled) {
console.log('ℹ️ [Qonversion] Purchase canceled by user');
Snackbar.show({
text: 'Purchase was canceled',
duration: Snackbar.LENGTH_SHORT,
});
} else if (result.isPending) {
console.log('⏳ [Qonversion] Purchase is pending');
Snackbar.show({
text: 'Purchase is pending approval',
duration: Snackbar.LENGTH_SHORT,
});
} else if (result.isError) {
console.error('❌ [Qonversion] Purchase failed with error:', result.error);
Alert.alert('Purchase Error', result.error?.description || 'An error occurred');
}

} catch (error: any) {
console.error('❌ [Qonversion] purchaseWithResult() call failed:', error);
Alert.alert('Error', error.message);
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
Expand Down
5 changes: 5 additions & 0 deletions ios/RNQonversion.mm
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ - (void)purchase:(nonnull NSString *)productId quantity:(double)quantity context
[self.impl purchase:productId quantity:quantity contextKeys:contextKeys promoOffer:promoOfferDict resolve:resolve reject:reject];
}

- (void)purchaseWithResult:(nonnull NSString *)productId quantity:(double)quantity contextKeys:(NSArray * _Nullable)contextKeys promoOffer:(JS::NativeQonversionModule::QPromoOfferDetails &)promoOffer offerId:(NSString * _Nullable)offerId applyOffer:(BOOL)applyOffer oldProductId:(NSString * _Nullable)oldProductId updatePolicyKey:(NSString * _Nullable)updatePolicyKey resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
NSDictionary *promoOfferDict = convertPromoOfferDetailsToDictionary(promoOffer);
[self.impl purchaseWithResult:productId quantity:quantity contextKeys:contextKeys promoOffer:promoOfferDict resolve:resolve reject:reject];
}

- (void)setDefinedProperty:(NSString *)property value:(NSString *)value {
[self.impl setDefinedProperty:property value:value];
}
Expand Down
7 changes: 7 additions & 0 deletions ios/RNQonversionImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ public class RNQonversionImpl: NSObject {
})
}

@objc
public func purchaseWithResult(_ productId: String, quantity: Int, contextKeys: [String], promoOffer: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
qonversionSandwich?.purchaseWithResult(productId, quantity: quantity, contextKeys: contextKeys, promoOffer: promoOffer, completion: { result, error in
self.handleResult(result: result, error: error, resolve: resolve, reject: reject)
})
}

@objc
public func setDefinedProperty(_ property: String, value: String) {
qonversionSandwich?.setDefinedProperty(property, value: value)
Expand Down
18 changes: 16 additions & 2 deletions src/QonversionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import UserProperties from './dto/UserProperties';
import PurchaseModel from './dto/PurchaseModel';
import PurchaseUpdateModel from './dto/PurchaseUpdateModel';
import PurchaseOptions from "./dto/PurchaseOptions";
import PurchaseResult from "./dto/PurchaseResult";
import SKProductDiscount from './dto/storeProducts/SKProductDiscount';
import PromotionalOffer from './dto/PromotionalOffer';

Expand Down Expand Up @@ -48,20 +49,33 @@ export interface QonversionApi {

getPromotionalOffer(product: Product, discount: SKProductDiscount): Promise<PromotionalOffer | null>;

/**
* Make a purchase and validate it through server-to-server using Qonversion's Backend.
* Returns a detailed PurchaseResult with status, entitlements, and store transaction details.
*
* @param product product to purchase
* @param options additional options for the purchase process.
* @returns the promise with the PurchaseResult containing status, entitlements, and transaction details
*
* @see [Making Purchases](https://documentation.qonversion.io/docs/making-purchases)
*/
purchaseWithResult(product: Product, options?: PurchaseOptions | undefined): Promise<PurchaseResult>

/**
* Make a purchase and validate it through server-to-server using Qonversion's Backend
* @param product product to purchase
* @param options additional options for the purchase process.
* @returns the promise with the user entitlements including the ones obtained by the purchase
*
* @deprecated Use {@link purchaseWithResult} function instead.
* @see [Making Purchases](https://documentation.qonversion.io/docs/making-purchases)
*/
purchaseProduct(product: Product, options?: PurchaseOptions | undefined): Promise<Map<string, Entitlement>>

/**
* Make a purchase and validate it through server-to-server using Qonversion's Backend.
*
* @deprecated Use {@link purchaseProduct} function instead.
* @deprecated Use {@link purchaseWithResult} function instead.
* @param purchaseModel necessary information for purchase
* @returns the promise with the user entitlements including the ones obtained by the purchase
* @see [Making Purchases](https://documentation.qonversion.io/docs/making-purchases)
Expand All @@ -73,7 +87,7 @@ export interface QonversionApi {
*
* Update (upgrade/downgrade) subscription on Google Play Store and validate it through server-to-server using Qonversion's Backend.
*
* @deprecated Use {@link purchaseProduct} function instead.
* @deprecated Use {@link purchaseWithResult} function instead.
* @param purchaseUpdateModel necessary information for purchase update
* @returns the promise with the user entitlements including updated ones.
* @see [Update policy](https://developer.android.com/google/play/billing/subscriptions#replacement-modes)
Expand Down
88 changes: 88 additions & 0 deletions src/dto/PurchaseResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Entitlement from './Entitlement';
import QonversionError from './QonversionError';
import StoreTransaction from './StoreTransaction';
import { PurchaseResultStatus, PurchaseResultSource } from './enums';

/**
* Represents the result of a purchase operation.
* Contains the status of the purchase, entitlements, and store transaction details.
*/
class PurchaseResult {
/**
* The status of the purchase operation.
*/
status: PurchaseResultStatus;

/**
* The user's entitlements after the purchase.
* May be null if the purchase failed or is pending.
*/
entitlements: Map<string, Entitlement> | null;

/**
* The error that occurred during the purchase, if any.
*/
error: QonversionError | null;

/**
* Indicates whether the entitlements were generated from a fallback source.
*/
isFallbackGenerated: boolean;

/**
* The source of the purchase result data.
*/
source: PurchaseResultSource;

/**
* The store transaction details from the native platform.
* Contains raw transaction information from Apple App Store or Google Play Store.
*/
storeTransaction: StoreTransaction | null;

constructor(
status: PurchaseResultStatus,
entitlements: Map<string, Entitlement> | null,
error: QonversionError | null,
isFallbackGenerated: boolean,
source: PurchaseResultSource,
storeTransaction: StoreTransaction | null,
) {
this.status = status;
this.entitlements = entitlements;
this.error = error;
this.isFallbackGenerated = isFallbackGenerated;
this.source = source;
this.storeTransaction = storeTransaction;
}

/**
* Returns true if the purchase was successful.
*/
get isSuccess(): boolean {
return this.status === PurchaseResultStatus.SUCCESS;
}

/**
* Returns true if the purchase was canceled by the user.
*/
get isCanceled(): boolean {
return this.status === PurchaseResultStatus.USER_CANCELED;
}

/**
* Returns true if the purchase is pending.
*/
get isPending(): boolean {
return this.status === PurchaseResultStatus.PENDING;
}

/**
* Returns true if an error occurred during the purchase.
*/
get isError(): boolean {
return this.status === PurchaseResultStatus.ERROR;
}
}

export default PurchaseResult;
61 changes: 61 additions & 0 deletions src/dto/StoreTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Represents a raw store transaction from the native platform.
* This is the transaction information as received from Apple App Store or Google Play Store.
*/
class StoreTransaction {
/**
* The unique identifier for this transaction.
*/
transactionId?: string;

/**
* The original transaction identifier.
* For subscriptions, this identifies the first transaction in the subscription chain.
*/
originalTransactionId?: string;

/**
* The date and time when the transaction occurred.
*/
transactionDate?: Date;

/**
* The store product identifier associated with this transaction.
*/
productId?: string;

/**
* The quantity of items purchased.
*/
quantity: number;

/**
* iOS only. The identifier of the promotional offer applied to this purchase.
*/
promoOfferId?: string;

/**
* Android only. The purchase token from Google Play.
*/
purchaseToken?: string;

constructor(
transactionId: string | undefined,
originalTransactionId: string | undefined,
transactionTimestamp: number | undefined,
productId: string | undefined,
quantity: number | undefined,
promoOfferId: string | undefined,
purchaseToken: string | undefined,
) {
this.transactionId = transactionId;
this.originalTransactionId = originalTransactionId;
this.transactionDate = transactionTimestamp ? new Date(transactionTimestamp) : undefined;
this.productId = productId;
this.quantity = quantity ?? 1;
this.promoOfferId = promoOfferId;
this.purchaseToken = purchaseToken;
}
}

export default StoreTransaction;
Loading