diff --git a/QonversionCapacitorPlugin.podspec b/QonversionCapacitorPlugin.podspec
index d6f4091..69c19a3 100644
--- a/QonversionCapacitorPlugin.podspec
+++ b/QonversionCapacitorPlugin.podspec
@@ -14,5 +14,5 @@ Pod::Spec.new do |s|
s.ios.deployment_target = '13.0'
s.dependency 'Capacitor'
s.swift_version = '5.1'
- s.dependency "QonversionSandwich", "5.1.6"
+ s.dependency "QonversionSandwich", "5.2.0"
end
diff --git a/android/build.gradle b/android/build.gradle
index 3e16529..95a06cf 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -61,7 +61,7 @@ dependencies {
implementation project(':capacitor-android')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation 'androidx.core:core-ktx:1.13.1'
- implementation "io.qonversion.sandwich:sandwich:5.1.6"
+ implementation "io.qonversion.sandwich:sandwich:5.2.0"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
diff --git a/example/ios/App/Podfile.lock b/example/ios/App/Podfile.lock
index 364abc4..7d80951 100644
--- a/example/ios/App/Podfile.lock
+++ b/example/ios/App/Podfile.lock
@@ -6,14 +6,14 @@ PODS:
- CapacitorCordova (6.1.2)
- CapacitorSplashScreen (6.0.2):
- Capacitor
- - Qonversion (5.12.4):
- - Qonversion/Main (= 5.12.4)
- - Qonversion/Main (5.12.4)
- - QonversionCapacitorPlugin (0.1.1):
+ - Qonversion (5.13.0):
+ - Qonversion/Main (= 5.13.0)
+ - Qonversion/Main (5.13.0)
+ - QonversionCapacitorPlugin (0.1.3):
- Capacitor
- - QonversionSandwich (= 5.1.6)
- - QonversionSandwich (5.1.6):
- - Qonversion (= 5.12.4)
+ - QonversionSandwich (= 5.2.0)
+ - QonversionSandwich (5.2.0):
+ - Qonversion (= 5.13.0)
DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
@@ -44,9 +44,9 @@ SPEC CHECKSUMS:
CapacitorCamera: ed022171dbf3853e68eec877b4d78995378af6b7
CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd
CapacitorSplashScreen: 250df9ef8014fac5c7c1fd231f0f8b1d8f0b5624
- Qonversion: cca480020597aa8bb74d9c5d0bf1916e68b8440e
- QonversionCapacitorPlugin: e1ef7bcf16b785b6edbe6c2f35a323fcb4bb07b4
- QonversionSandwich: 5ed2ecfc84e4af49e49ddb1250ccf00dc9a7c0a5
+ Qonversion: 37addeba74c5b328de9e1173b580c971b6d764ec
+ QonversionCapacitorPlugin: 96d479bb23452fd216f8119d8375fc41e31822e2
+ QonversionSandwich: 3ffa118b7214ebd2dcd3f3a1a0a33a39f9c48c8e
PODFILE CHECKSUM: 238528980101a97d01a6a29581a5e0d56d489a11
diff --git a/example/src/index.html b/example/src/index.html
index ced23e5..ad8e9bc 100644
--- a/example/src/index.html
+++ b/example/src/index.html
@@ -21,6 +21,11 @@
diff --git a/example/src/js/index.js b/example/src/js/index.js
index e6041aa..308653f 100644
--- a/example/src/js/index.js
+++ b/example/src/js/index.js
@@ -44,6 +44,34 @@ window.getProducts = async () => {
console.log('Qonversion products:', products);
}
+window.getPromoOffer = async () => {
+ const productId = document.getElementById('product-id-promo').value;
+ const discountId = document.getElementById('discount-id-promo').value;
+ const products = await Qonversion.getSharedInstance().products();
+
+ const product = products.get(productId);
+ if (!product) {
+ console.log('Qonversion product not found: ', productId);
+ return;
+ }
+
+ const discount = product.skProduct.discounts.find(discount => discount.identifier === discountId);
+ if (!discount) {
+ console.log('Qonversion discount not found for requested product: ', discountId);
+ return;
+ }
+
+ try {
+ const promoOffer = await Qonversion.getSharedInstance().getPromotionalOffer(product, discount);
+ console.log('Qonversion getPromotionalOffer:', promoOffer);
+
+ const entitlements = await Qonversion.getSharedInstance().purchaseProduct(product, new PurchaseOptionsBuilder().setPromotionalOffer(promoOffer).build());
+ console.log('Entitlements: ', entitlements);
+ } catch (e) {
+ console.log('Qonversion getPromotionalOffer failed', e);
+ }
+}
+
window.getRemoteConfig = async () => {
const contextKey = document.getElementById('context-key').value;
const key = contextKey?.length > 0 ? contextKey : undefined;
diff --git a/ios/Sources/QonversionPlugin/QonversionPlugin.swift b/ios/Sources/QonversionPlugin/QonversionPlugin.swift
index 8bd49c6..6f19549 100644
--- a/ios/Sources/QonversionPlugin/QonversionPlugin.swift
+++ b/ios/Sources/QonversionPlugin/QonversionPlugin.swift
@@ -11,6 +11,7 @@ public class QonversionPlugin: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "identify", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "products", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "getPromotionalOffer", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "purchase", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "promoPurchase", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "checkEntitlements", returnType: CAPPluginReturnPromise),
@@ -80,14 +81,24 @@ public class QonversionPlugin: CAPPlugin, CAPBridgedPlugin {
qonversionSandwich?.products(getDefaultCompletion(call))
}
+ @objc func getPromotionalOffer(_ call: CAPPluginCall) {
+ guard let productId = call.getString("productId"),
+ let discountId = call.getString("discountId") else {
+ return call.noNecessaryDataError()
+ }
+
+ qonversionSandwich?.getPromotionalOffer(productId, productDiscountId:discountId, completion: getDefaultCompletion(call))
+ }
+
@objc func purchase(_ call: CAPPluginCall) {
guard let productId = call.getString("productId") else {
return call.noNecessaryDataError()
}
let quantity = call.getInt("quantity") ?? 1
- let contextKeys = call.getArray("contextKeys")?.capacitor.replacingNullValues().compactMap({$0}) as? [String] ?? []
+ let contextKeys = call.getArray("contextKeys")?.capacitor.replacingNullValues().compactMap({$0}) as? [String] ?? []
+ let promoOffer = call.getObject("promoOffer") ?? [:]
- qonversionSandwich?.purchase(productId, quantity:quantity, contextKeys:contextKeys, completion: getDefaultCompletion(call))
+ qonversionSandwich?.purchase(productId, quantity:quantity, contextKeys:contextKeys, promoOffer:promoOffer, completion: getDefaultCompletion(call))
}
@objc func promoPurchase(_ call: CAPPluginCall) {
diff --git a/src/QonversionApi.ts b/src/QonversionApi.ts
index 392ec70..7247320 100644
--- a/src/QonversionApi.ts
+++ b/src/QonversionApi.ts
@@ -10,6 +10,8 @@ import {AttributionProvider, UserPropertyKey} from './dto/enums';
import {UserProperties} from './dto/UserProperties';
import {EntitlementsUpdateListener} from './dto/EntitlementsUpdateListener';
import {PromoPurchasesListener} from './dto/PromoPurchasesListener';
+import {SKProductDiscount} from './dto/storeProducts/SKProductDiscount';
+import {PromotionalOffer} from './dto/PromotionalOffer';
export interface QonversionApi {
/**
@@ -24,6 +26,19 @@ export interface QonversionApi {
*/
syncStoreKit2Purchases(): void;
+ /**
+ * iOS only.
+ * Retrieve the promotional offer for the product if it exists.
+ * Make sure to call this function before displaying product details to the user.
+ * The generated signature for the promotional offer is valid for a single transaction.
+ * If the purchase fails, you need to call this function again to obtain a new promotional offer signature.
+ * Use this signature to complete the purchase through the purchase function, along with the purchase options object.
+ * @param product - product you want to purchase.
+ * @param discount - discount to create promotional offer signature.
+ * @returns the promise with the PromotionalOffer.
+ */
+ getPromotionalOffer(product: Product, discount: SKProductDiscount): Promise
;
+
/**
* Make a purchase and validate it through server-to-server using Qonversion's Backend
* @param product product to purchase
diff --git a/src/QonversionNativePlugin.ts b/src/QonversionNativePlugin.ts
index 9321a2e..d386959 100644
--- a/src/QonversionNativePlugin.ts
+++ b/src/QonversionNativePlugin.ts
@@ -2,10 +2,12 @@ import {
QEntitlement,
QOfferings,
QProduct,
+ QPromotionalOffer,
QRemoteConfig,
QRemoteConfigList,
QTrialIntroEligibility,
- QUser, QUserProperties
+ QUser,
+ QUserProperties
} from './internal/Mapper';
export interface QonversionNativePlugin {
@@ -26,6 +28,8 @@ export interface QonversionNativePlugin {
storeSdkInfo(params: {source: string, version: string}): void;
+ getPromotionalOffer(params: {productId: string, discountId: string | undefined}): Promise;
+
purchase(params: {
productId: string,
quantity?: number,
@@ -34,6 +38,7 @@ export interface QonversionNativePlugin {
applyOffer?: boolean | undefined,
oldProductId?: string | undefined,
updatePolicyKey?: string | null | undefined,
+ promoOffer?: Object | null,
}): Promise | null | undefined>;
products(): Promise | null | undefined>;
diff --git a/src/dto/PromotionalOffer.ts b/src/dto/PromotionalOffer.ts
new file mode 100644
index 0000000..57d33e7
--- /dev/null
+++ b/src/dto/PromotionalOffer.ts
@@ -0,0 +1,15 @@
+import {SKProductDiscount} from './storeProducts/SKProductDiscount';
+import {SKPaymentDiscount} from './storeProducts/SKPaymentDiscount';
+
+export class PromotionalOffer {
+ public readonly productDiscount: SKProductDiscount;
+ public readonly paymentDiscount: SKPaymentDiscount;
+
+ constructor (
+ productDiscount: SKProductDiscount,
+ paymentDiscount: SKPaymentDiscount
+ ) {
+ this.productDiscount = productDiscount;
+ this.paymentDiscount = paymentDiscount;
+ }
+}
diff --git a/src/dto/PurchaseOptions.ts b/src/dto/PurchaseOptions.ts
index f1f9bd8..e033a86 100644
--- a/src/dto/PurchaseOptions.ts
+++ b/src/dto/PurchaseOptions.ts
@@ -1,5 +1,6 @@
import {Product} from "./Product";
import {PurchaseUpdatePolicy} from "./enums";
+import {PromotionalOffer} from './PromotionalOffer';
export class PurchaseOptions {
public readonly offerId: string | null;
@@ -8,6 +9,7 @@ export class PurchaseOptions {
public readonly updatePolicy: PurchaseUpdatePolicy | null;
public readonly contextKeys: string[] | null;
public readonly quantity: number;
+ public readonly promotionalOffer: PromotionalOffer | null;
constructor (
offerId: string | null,
@@ -15,7 +17,8 @@ export class PurchaseOptions {
oldProduct: Product | null,
updatePolicy: PurchaseUpdatePolicy | null,
contextKeys: string[] | null,
- quantity: number
+ quantity: number,
+ promotionalOffer: PromotionalOffer | null
) {
this.offerId = offerId;
this.applyOffer = applyOffer;
@@ -23,5 +26,6 @@ export class PurchaseOptions {
this.updatePolicy = updatePolicy;
this.contextKeys = contextKeys;
this.quantity = quantity;
+ this.promotionalOffer = promotionalOffer;
}
}
diff --git a/src/dto/PurchaseOptionsBuilder.ts b/src/dto/PurchaseOptionsBuilder.ts
index 54d4c94..b096405 100644
--- a/src/dto/PurchaseOptionsBuilder.ts
+++ b/src/dto/PurchaseOptionsBuilder.ts
@@ -2,6 +2,7 @@ import {Product} from './Product';
import {PurchaseUpdatePolicy} from './enums';
import {ProductOfferDetails} from './storeProducts/ProductOfferDetails';
import {PurchaseOptions} from './PurchaseOptions';
+import {PromotionalOffer} from './PromotionalOffer';
export class PurchaseOptionsBuilder {
private offerId: string | null = null;
@@ -10,6 +11,7 @@ export class PurchaseOptionsBuilder {
private updatePolicy: PurchaseUpdatePolicy | null = null;
private contextKeys: string[] | null = null;
private quantity: number = 1;
+ private promoOffer: PromotionalOffer | null = null;
/**
* iOS only.
@@ -95,11 +97,30 @@ export class PurchaseOptionsBuilder {
return this;
}
+ /**
+ * Set the promotional offer details.
+ *
+ * @param promoOffer promotional offer details.
+ * @return builder instance for chain calls.
+ */
+ setPromotionalOffer(promoOffer: PromotionalOffer): PurchaseOptionsBuilder {
+ this.promoOffer = promoOffer;
+ return this;
+ }
+
/**
* Generate {@link PurchaseOptions} instance with all the provided options.
* @return the complete {@link PurchaseOptions} instance.
*/
build(): PurchaseOptions {
- return new PurchaseOptions(this.offerId, this.applyOffer, this.oldProduct, this.updatePolicy, this.contextKeys, this.quantity);
+ return new PurchaseOptions(
+ this.offerId,
+ this.applyOffer,
+ this.oldProduct,
+ this.updatePolicy,
+ this.contextKeys,
+ this.quantity,
+ this.promoOffer
+ );
}
}
diff --git a/src/dto/Transaction.ts b/src/dto/Transaction.ts
index f1352da..f83a8f0 100644
--- a/src/dto/Transaction.ts
+++ b/src/dto/Transaction.ts
@@ -10,6 +10,7 @@ export class Transaction {
expirationDate?: Date;
transactionRevocationDate?: Date;
offerCode?: string;
+ promoOfferId?: string;
constructor(
originalTransactionId: string,
@@ -21,6 +22,7 @@ export class Transaction {
expirationTimestamp: number | undefined,
transactionRevocationTimestamp: number | undefined,
offerCode: string | undefined,
+ promoOfferId: string | undefined,
) {
this.originalTransactionId = originalTransactionId;
this.transactionId = transactionId;
@@ -31,5 +33,6 @@ export class Transaction {
this.expirationDate = expirationTimestamp ? new Date(expirationTimestamp) : undefined;
this.transactionRevocationDate = transactionRevocationTimestamp ? new Date(transactionRevocationTimestamp) : undefined;
this.offerCode = offerCode;
+ this.promoOfferId = promoOfferId;
}
}
diff --git a/src/dto/storeProducts/SKPaymentDiscount.ts b/src/dto/storeProducts/SKPaymentDiscount.ts
new file mode 100644
index 0000000..73f3238
--- /dev/null
+++ b/src/dto/storeProducts/SKPaymentDiscount.ts
@@ -0,0 +1,21 @@
+export class SKPaymentDiscount {
+ identifier: string;
+ keyIdentifier: string;
+ nonce: string;
+ signature: string;
+ timestamp: number;
+
+ constructor (
+ identifier: string,
+ keyIdentifier: string,
+ nonce: string,
+ signature: string,
+ timestamp: number,
+ ) {
+ this.identifier = identifier;
+ this.keyIdentifier = keyIdentifier;
+ this.nonce = nonce;
+ this.signature = signature;
+ this.timestamp = timestamp;
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 817eee9..361d231 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,6 +5,7 @@ export * from './dto/IntroEligibility';
export * from './dto/Offering';
export * from './dto/Offerings';
export * from './dto/Product';
+export * from './dto/PromotionalOffer';
export * from './dto/QonversionError';
export * from './dto/User';
export * from './dto/UserProperty';
@@ -28,6 +29,7 @@ export * from './dto/PurchaseOptions';
export * from './dto/PurchaseOptionsBuilder';
export * from './dto/EntitlementsUpdateListener';
export * from './dto/PromoPurchasesListener';
+export * from './dto/storeProducts/SKPaymentDiscount';
export * from './dto/storeProducts/SKProduct';
export * from './dto/storeProducts/SKProductDiscount';
export * from './dto/storeProducts/SKSubscriptionPeriod';
diff --git a/src/internal/Mapper.ts b/src/internal/Mapper.ts
index 94579dc..4b09c8f 100644
--- a/src/internal/Mapper.ts
+++ b/src/internal/Mapper.ts
@@ -47,6 +47,8 @@ import {ProductInAppDetails} from "../dto/storeProducts/ProductInAppDetails";
import {ProductPrice} from "../dto/storeProducts/ProductPrice";
import {ProductPricingPhase} from "../dto/storeProducts/ProductPricingPhase";
import {ProductInstallmentPlanDetails} from '../dto/storeProducts/ProductInstallmentPlanDetails';
+import {PromotionalOffer} from '../dto/PromotionalOffer';
+import {SKPaymentDiscount} from '../dto/storeProducts/SKPaymentDiscount';
export type QProduct = {
id: string;
@@ -104,6 +106,11 @@ type QProductInstallmentPlanDetails = {
subsequentCommitmentPaymentsCount: number;
}
+export type QPromotionalOffer = {
+ productDiscount: QProductDiscount,
+ paymentDiscount: QPaymentDiscount,
+}
+
type QProductOfferDetails = {
basePlanId: string,
offerId?: string | null,
@@ -185,6 +192,14 @@ type QProductDiscount = {
priceLocale: QLocale;
};
+type QPaymentDiscount = {
+ identifier: string;
+ keyIdentifier: string;
+ nonce: string;
+ signature: string;
+ timestamp: number;
+};
+
type QLocale = {
currencySymbol: string | null;
currencyCode: string | null;
@@ -226,6 +241,7 @@ type QTransaction = {
environment: string;
ownershipType: string;
type: string;
+ promoOfferId: string;
}
export type QOfferings = {
@@ -286,6 +302,19 @@ export type QUserProperties = {
const priceMicrosRatio = 1000000;
class Mapper {
+ static convertPromoOffer(
+ promoOffer: QPromotionalOffer | null | undefined
+ ): PromotionalOffer | null {
+ if (!promoOffer) {
+ return null;
+ }
+
+ const productDiscount = this.convertProductDiscount(promoOffer.productDiscount);
+ const paymentDiscount = this.convertPaymentDiscount(promoOffer.paymentDiscount);
+
+ return new PromotionalOffer(productDiscount, paymentDiscount);
+ }
+
static convertEntitlements(
entitlements: Record | null | undefined
): Map {
@@ -364,6 +393,7 @@ class Mapper {
transaction.expirationTimestamp,
transaction.transactionRevocationTimestamp,
transaction.offerCode,
+ transaction.promoOfferId,
);
}
@@ -907,6 +937,10 @@ class Mapper {
);
}
+ static convertPaymentDiscount(discount: QPaymentDiscount): SKPaymentDiscount {
+ return new SKPaymentDiscount(discount.identifier, discount.keyIdentifier, discount.nonce, discount.signature, discount.timestamp)
+ }
+
static convertProductDiscount(discount: QProductDiscount): SKProductDiscount {
let subscriptionPeriod: SKSubscriptionPeriod | undefined = undefined;
if (discount.subscriptionPeriod != null) {
diff --git a/src/internal/QonversionInternal.ts b/src/internal/QonversionInternal.ts
index 9483d48..4497205 100644
--- a/src/internal/QonversionInternal.ts
+++ b/src/internal/QonversionInternal.ts
@@ -17,6 +17,8 @@ import {RemoteConfigList} from '../dto/RemoteConfigList';
import {QonversionApi} from '../QonversionApi';
import {QonversionNativePlugin} from '../QonversionNativePlugin';
import {PurchaseOptionsBuilder} from '../dto/PurchaseOptionsBuilder';
+import {SKProductDiscount} from '../dto/storeProducts/SKProductDiscount';
+import {PromotionalOffer} from '../dto/PromotionalOffer';
const sdkVersion = "0.1.3";
@@ -55,6 +57,20 @@ export default class QonversionInternal implements QonversionApi {
}
}
+ async getPromotionalOffer(product: Product, discount: SKProductDiscount): Promise {
+ if (isAndroid()) {
+ return null;
+ }
+
+ const promoOffer = await QonversionNative.getPromotionalOffer({
+ productId: product.qonversionID,
+ discountId: discount.identifier,
+ });
+ const mappedPromoOffer: PromotionalOffer | null = Mapper.convertPromoOffer(promoOffer);
+
+ return mappedPromoOffer;
+ }
+
async purchaseProduct(product: Product, options: PurchaseOptions | undefined): Promise