Skip to content

Commit

Permalink
Merge pull request #19 from qonversion/feature/sc-33816/promoOffers
Browse files Browse the repository at this point in the history
Promo offers implementation for iOS.
  • Loading branch information
suriksarkisyan authored Nov 22, 2024
2 parents 020d409 + 793b747 commit ca04b44
Show file tree
Hide file tree
Showing 17 changed files with 209 additions and 19 deletions.
2 changes: 1 addition & 1 deletion QonversionCapacitorPlugin.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
20 changes: 10 additions & 10 deletions example/ios/App/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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`)"
Expand Down Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions example/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ <h1>Qonversion Capacitor Sample Project</h1>
<button onclick="purchase()" type="button" class="qon-button">Purchase</button>
</div>
<button onclick="getProducts()" type="button" class="qon-button top-margin">Get Products</button>
<div class="row top-margin">
<input placeholder="Product id" id="product-id-promo" type="text" class="qon-input" name="product-id" />
<input placeholder="Discount id" id="discount-id-promo" type="text" class="qon-input" name="discount-id" />
<button onclick="getPromoOffer()" type="button" class="qon-button">Get Promo Offer</button>
</div>
<div class="row top-margin">
<input placeholder="context key" id="context-key" type="text" class="qon-input" name="context-key" />
<button onclick="getRemoteConfig()" type="button" class="qon-button">Get Remote Config</button>
Expand Down
25 changes: 25 additions & 0 deletions example/src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,31 @@ 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);
} 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;
Expand Down
15 changes: 13 additions & 2 deletions ios/Sources/QonversionPlugin/QonversionPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 15 additions & 0 deletions src/QonversionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -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<PromotionalOffer | null>;

/**
* Make a purchase and validate it through server-to-server using Qonversion's Backend
* @param product product to purchase
Expand Down
7 changes: 6 additions & 1 deletion src/QonversionNativePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {
QEntitlement,
QOfferings,
QProduct,
QPromotionalOffer,
QRemoteConfig,
QRemoteConfigList,
QTrialIntroEligibility,
QUser, QUserProperties
QUser,
QUserProperties
} from './internal/Mapper';

export interface QonversionNativePlugin {
Expand All @@ -26,6 +28,8 @@ export interface QonversionNativePlugin {

storeSdkInfo(params: {source: string, version: string}): void;

getPromotionalOffer(params: {productId: string, discountId: string | undefined}): Promise<QPromotionalOffer>;

purchase(params: {
productId: string,
quantity?: number,
Expand All @@ -34,6 +38,7 @@ export interface QonversionNativePlugin {
applyOffer?: boolean | undefined,
oldProductId?: string | undefined,
updatePolicyKey?: string | null | undefined,
promoOffer?: Object | null,
}): Promise<Record<string, QEntitlement> | null | undefined>;

products(): Promise<Record<string, QProduct> | null | undefined>;
Expand Down
15 changes: 15 additions & 0 deletions src/dto/PromotionalOffer.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 5 additions & 1 deletion src/dto/PurchaseOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Product} from "./Product";
import {PurchaseUpdatePolicy} from "./enums";
import {PromotionalOffer} from './PromotionalOffer';

export class PurchaseOptions {
public readonly offerId: string | null;
Expand All @@ -8,20 +9,23 @@ 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,
applyOffer: boolean,
oldProduct: Product | null,
updatePolicy: PurchaseUpdatePolicy | null,
contextKeys: string[] | null,
quantity: number
quantity: number,
promotionalOffer: PromotionalOffer | null
) {
this.offerId = offerId;
this.applyOffer = applyOffer;
this.oldProduct = oldProduct;
this.updatePolicy = updatePolicy;
this.contextKeys = contextKeys;
this.quantity = quantity;
this.promotionalOffer = promotionalOffer;
}
}
23 changes: 22 additions & 1 deletion src/dto/PurchaseOptionsBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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
);
}
}
3 changes: 3 additions & 0 deletions src/dto/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class Transaction {
expirationDate?: Date;
transactionRevocationDate?: Date;
offerCode?: string;
promoOfferId?: string;

constructor(
originalTransactionId: string,
Expand All @@ -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;
Expand All @@ -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;
}
}
21 changes: 21 additions & 0 deletions src/dto/storeProducts/SKPaymentDiscount.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
Loading

0 comments on commit ca04b44

Please sign in to comment.