Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support to iOS subscription offers #455

Merged
merged 6 commits into from
Apr 18, 2019
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
30 changes: 30 additions & 0 deletions apple.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,33 @@ export interface IAPReceipt {
*/
price_consent_status?: SubscriptionPriceConsentStatus
}

/**
* Payment discount interface @see https://developer.apple.com/documentation/storekit/skpaymentdiscount?language=objc
*/
export interface PaymentDiscount {
/**
* A string used to uniquely identify a discount offer for a product.
*/
identifier: string

/**
* A string that identifies the key used to generate the signature.
*/
keyIdentifier: string

/**
* A universally unique ID (UUID) value that you define.
*/
nonce: string

/**
* A UTF-8 string representing the properties of a specific discount offer, cryptographically signed.
*/
signature: string

/**
* The date and time of the signature's creation in milliseconds, formatted in Unix epoch time.
*/
timestamp: number
}
11 changes: 11 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ export function buySubscription(sku: string, oldSku?: string, prorationMode?: nu
*/
export function buyProduct(sku: string) : Promise<ProductPurchase>;

/**
* Buy a product with offer
*
* @param {string} sku The product unique identifier
* @param {string} forUser An user identifier on your service (username or user id)
* @param {Apple.PaymentDiscount} withOffer The offer information
*
* @returns {Promise<void>}
*/
export function buyProductWithOfferIOS(sku: string, forUser: string, withOffer: Apple.PaymentDiscount) : Promise<void>;

/**
* Buy a product with a specified quantity (iOS only)
* @param {string} sku The product's sku/ID
Expand Down
23 changes: 23 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,29 @@ export const buyPromotedProduct = () => Platform.select({
android: async() => Promise.resolve(),
})();

/**
* Buy products or subscriptions with offers (iOS only)
*
* Runs the payment process with some infor you must fetch
* from your server.
* @param {string} sku The product identifier
* @param {string} forUser An user identifier on you system
* @param {object} withOffer The offer information
* @param {string} withOffer.identifier The offer identifier
* @param {string} withOffer.keyIdentifier Key identifier that it uses to generate the signature
* @param {string} withOffer.nonce An UUID returned from the server
* @param {string} withOffer.signature The actual signature returned from the server
* @param {number} withOffer.timestamp The timestamp of the signature
* @returns {Promise}
*/
export const buyProductWithOfferIOS = (sku, forUser, withOffer) => Platform.select({
ios: () => {
checkNativeiOSAvailable();
return RNIapIos.buyProductWithOffer(sku, forUser, withOffer);
},
android: () => Promise.resolve(),
})();

/**
* Validate receipt for iOS.
* @param {object} receiptBody the receipt body to send to apple server.
Expand Down
32 changes: 32 additions & 0 deletions index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ declare module.exports: {
getSubscriptions(string[]): Promise<Subscription<string>[]>,
initConnection(): Promise<string>,
buyProduct: string => Promise<ProductPurchase>,
buydProductWithOffer: (sku: string, forUser: string, withOffer: PaymentDiscount) => Promise<void>,
endConnection: () => Promise<void>,
consumeAllItems(): Promise<void>,
getPurchaseHistory(): Promise<Purchase[]>,
Expand Down Expand Up @@ -90,6 +91,37 @@ declare module.exports: {
): EmitterSubscription,
}

/**
* Payment discount interface @see https://developer.apple.com/documentation/storekit/skpaymentdiscount?language=objc
*/
declare type PaymentDiscount = {
/**
* A string used to uniquely identify a discount offer for a product.
*/
identifier: string

/**
* A string that identifies the key used to generate the signature.
*/
keyIdentifier: string

/**
* A universally unique ID (UUID) value that you define.
*/
nonce: string

/**
* A UTF-8 string representing the properties of a specific discount offer, cryptographically signed.
*/
signature: string

/**
* The date and time of the signature's creation in milliseconds, formatted in Unix epoch time.
*/
timestamp: number
}


// TODO: the following definitions should be more specific

export type AppleReceiptValidationResponse = any
Expand Down
120 changes: 119 additions & 1 deletion ios/RNIapIos.m
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,39 @@ - (BOOL)shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)produ
}
}

RCT_EXPORT_METHOD(buyProductWithOffer:(NSString*)sku
forUser:(NSString*)usernameHash
withOffer:(NSDictionary*)discountOffer
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
autoReceiptConform = true;
SKProduct *product;
for (SKProduct *p in validProducts) {
if([sku isEqualToString:p.productIdentifier]) {
product = p;
break;
}
}
if (product) {
SKPaymentDiscount *discount = [[SKPaymentDiscount alloc]
initWithIdentifier:discountOffer[@"identifier"]
keyIdentifier:discountOffer[@"keyIdentifier"]
nonce:[[NSUUID alloc] initWithUUIDString:discountOffer[@"nonce"]]
signature:discountOffer[@"signature"]
timestamp:discountOffer[@"timestamp"]
];

SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.applicationUsername = usernameHash;
payment.paymentDiscount = discount;

[[SKPaymentQueue defaultQueue] addPayment:payment];
[self addPromiseForKey:RCTKeyForInstance(payment.productIdentifier) resolve:resolve reject:reject];
} else {
reject(@"E_DEVELOPER_ERROR", @"Invalid product ID.", nil);
}
}

RCT_EXPORT_METHOD(buyProductWithQuantityIOS:(NSString*)sku
quantity:(NSInteger*)quantity
resolve:(RCTPromiseResolveBlock)resolve
Expand Down Expand Up @@ -449,6 +482,11 @@ -(NSDictionary*)getProductObject:(SKProduct *)product {
currencyCode = product.priceLocale.currencyCode;
}

NSArray *discounts;
if (@available(iOS 12.2, *)) {
discounts = [self getDiscountData:[product.discounts copy]];
}

NSDictionary *obj = [NSDictionary dictionaryWithObjectsAndKeys:
product.productIdentifier, @"productId",
[product.price stringValue], @"price",
Expand All @@ -463,12 +501,92 @@ -(NSDictionary*)getProductObject:(SKProduct *)product {
introductoryPricePaymentMode, @"introductoryPricePaymentModeIOS",
introductoryPriceNumberOfPeriods, @"introductoryPriceNumberOfPeriodsIOS",
introductoryPriceSubscriptionPeriod, @"introductoryPriceSubscriptionPeriodIOS",
discounts, @"discounts",
nil
];
];

return obj;
}

- (NSMutableArray *)getDiscountData:(NSArray *)discounts {
NSMutableArray *mappedDiscounts = [NSMutableArray arrayWithCapacity:[discounts count]];
NSString *localizedPrice;
NSString *paymendMode;
NSString *subscriptionPeriods;
NSString *discountType;

if (@available(iOS 11.2, *)) {
for(SKProductDiscount *discount in discounts) {
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = NSNumberFormatterCurrencyStyle;
formatter.locale = discount.priceLocale;
localizedPrice = [formatter stringFromNumber:discount.price];
NSString *numberOfPeriods;

switch (discount.paymentMode) {
case SKProductDiscountPaymentModeFreeTrial:
paymendMode = @"FREETRIAL";
numberOfPeriods = [@(discount.subscriptionPeriod.numberOfUnits) stringValue];
break;
case SKProductDiscountPaymentModePayAsYouGo:
paymendMode = @"PAYASYOUGO";
numberOfPeriods = [@(discount.numberOfPeriods) stringValue];
break;
case SKProductDiscountPaymentModePayUpFront:
paymendMode = @"PAYUPFRONT";
numberOfPeriods = [@(discount.subscriptionPeriod.numberOfUnits) stringValue];
break;
default:
paymendMode = @"";
numberOfPeriods = @"0";
break;
}

switch (discount.subscriptionPeriod.unit) {
case SKProductPeriodUnitDay:
subscriptionPeriods = @"DAY";
break;
case SKProductPeriodUnitWeek:
subscriptionPeriods = @"WEEK";
break;
case SKProductPeriodUnitMonth:
subscriptionPeriods = @"MONTH";
break;
case SKProductPeriodUnitYear:
subscriptionPeriods = @"YEAR";
break;
default:
subscriptionPeriods = @"";
}

switch (discount.type) {
case SKProductDiscountTypeIntroductory:
discountType = @"INTRODUCTORY";
break;
case SKProductDiscountTypeSubscription:
discountType = @"SUBSCRIPTION";
break;
default:
discountType = @"";
break;
}

[mappedDiscounts addObject:[NSDictionary dictionaryWithObjectsAndKeys:
discount.identifier, @"identifier",
discountType, @"type",
numberOfPeriods, @"numberOfPeriods",
discount.price, @"price",
localizedPrice, @"localizedPrice",
paymendMode, @"paymentMode",
subscriptionPeriods, @"subscriptionPeriod",
nil
]];
}
}

return mappedDiscounts;
}

- (NSDictionary *)getPurchaseData:(SKPaymentTransaction *)transaction {
NSData *receiptData;
if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_7_0) {
Expand Down