Skip to content

Commit

Permalink
Exporting new methods on Sk2 (#1960)
Browse files Browse the repository at this point in the history
* Exporting new methods on Sk2

* swiftlint

* Added migration documentation

* lint

Co-authored-by: Andres Aguilar <andres.aguilar@nfl.com>
  • Loading branch information
andresesfm and Andres Aguilar authored Sep 14, 2022
1 parent 8bbee73 commit d62eebc
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 51 deletions.
File renamed without changes.
62 changes: 62 additions & 0 deletions docs/docs/migrate_to_11.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Migrating to 11.0.0

Version 11.0.0 is centered around implementing iOS Storekit 2.

I've worked hard on keeping backward compatibility. However there are things that don't translate directly to previous versions.

## API changes

These are changes that you'd need to make even if you are not going to use Storekit 2:

- `requestPurchaseWithOfferIOS` and `requestPurchaseWithQuantityIOS` are now part of `requestPurchase` by simply passing the appropriate parameters

- Methods that are exclusive to a platform, have been moved to nested objects `IapAndroid`, `IapAmazon`, `IapIos`, `IapIosSk2`. So for example `validateReceiptAndroid` is now available as:

```ts
import {IapAndroid} from 'react-native-iap'
...
IapAndroid.validateReceiptAndroid(...)
```

In particular the following methods are avaiable only on Sk2 : `sync`,`isEligibleForIntroOffer`, `subscriptionStatus`, `currentEntitlement`,`latestTransaction` should be called as this:

```ts
import {IapIosSk2} from 'react-native-iap'
...
IapIosSk2.isEligibleForIntroOffer(...)
```

This allows for greater flexibility to use methods that are specific to a platform but the others don't offer. All the other common methods are still called in the same way as before

## Using Storekit 2

Storekit 2 requiers iOS 15 as a minimum. If your app supports older iOS versions, you'll have to consider the endgecases of jumping back and forth. The library will use the old implementation (Storekit 1) as a default on devices with older versions of iOS

### How do I know what's the minimum version of iOS my app supports?

Open `ios/Podfile` file
and look for the following line:

```
platform :ios, '15.0'
```

### How do I enable the use of Storekit 2

Call `enableStorekit2` before you initialize your connection as follows:

```ts
enableStorekit2()
await initConnection()
...
```

### Buying items for user

When calling `requestPurchase`:
The name of this parameter has changed to match the new API
`applicationUsername` -> `appAccountToken`

## No longer available in Sk2:

purchase promoted product. I haven't found the equivalent of promoted product purchase in the new SDK.
30 changes: 30 additions & 0 deletions ios/IapSerializationUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,36 @@ func serialize(_ si: Product.SubscriptionInfo?) -> [String: Any?]? {
"subscriptionPeriod": si.subscriptionPeriod
]
}
@available(iOS 15.0, *)
func serialize(_ s: Product.SubscriptionInfo.Status?) -> [String: Any?]? {
guard let s = s else {return nil}
return ["state": serialize( s.state)
// "renewalInfo": serialize(s.renewalInfo),
// "transaction": serialize(s.transaction),
]
}

@available(iOS 15.0, *)
func serialize(_ rs: Product.SubscriptionInfo.RenewalState?) -> String? {
guard let rs = rs else {return nil}
switch rs {
case .expired: return "expired"
case .inBillingRetryPeriod: return "inBillingRetryPeriod"
case .inGracePeriod: return "inGracePeriod"
case .revoked: return "revoked"
case .subscribed: return "subscribed"
default:
return nil
}
}

@available(iOS 15.0, *)
func serialize(_ ri: Product.SubscriptionInfo.RenewalInfo?) -> [String: Any?]? {
guard let ri = ri else {return nil}
return ["signedDate": ri.signedDate
]
}

@available(iOS 15.0, *)
func serialize(_ so: Product.SubscriptionOffer?) -> [String: Any?]? {
guard let so = so else {return nil}
Expand Down
5 changes: 5 additions & 0 deletions ios/RNIapIosSk2.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ @interface RCT_EXTERN_MODULE (RNIapIosSk2, NSObject)
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(subscriptionStatus:
(NSString*)sku
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(currentEntitlement:
(NSString*)sku
resolve:(RCTPromiseResolveBlock)resolve
Expand Down
112 changes: 63 additions & 49 deletions ios/RNIapIosSk2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,75 +306,87 @@ class RNIapIosSk2: RCTEventEmitter {
}
}

@objc public func isEligibleForIntroOffer( // TODO: new method
@objc public func isEligibleForIntroOffer(
_ groupID: String,
resolve: @escaping RCTPromiseResolveBlock = { _ in },
reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
) async {
let isEligibleForIntroOffer = await Product.SubscriptionInfo.isEligibleForIntroOffer(for: groupID)
resolve(isEligibleForIntroOffer)
) {
Task {
let isEligibleForIntroOffer = await Product.SubscriptionInfo.isEligibleForIntroOffer(for: groupID)
resolve(isEligibleForIntroOffer)
}
}

@objc public func subscriptionStatus( // TODO: new method
@objc public func subscriptionStatus(
_ sku: String,
resolve: @escaping RCTPromiseResolveBlock = { _ in },
reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
) async {
do {
let status = try await products[sku]?.subscription?.status
resolve(status)
} catch {
reject("", "", error)
) {
Task {
do {
let status: [Product.SubscriptionInfo.Status]? = try await products[sku]?.subscription?.status
guard let status = status else {
resolve(nil)
return
}
resolve(status.map({s in serialize(s)}))
} catch {
reject(IapErrors.E_UNKNOWN.rawValue, "Error getting subscription status", error)
}
}
}

@objc public func currentEntitlement( // TODO: new method
@objc public func currentEntitlement(
_ sku: String,
resolve: @escaping RCTPromiseResolveBlock = { _ in },
reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
) async {
if let product = products[sku] {
if let result = await product.currentEntitlement {
do {
// Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
let transaction = try checkVerified(result)
resolve(serialize(transaction))
} catch StoreError.failedVerification {
reject(IapErrors.E_UNKNOWN.rawValue, "Failed to verify transaction for sku \(sku)", StoreError.failedVerification)
} catch {
debugMessage(error)
reject(IapErrors.E_UNKNOWN.rawValue, "Error fetching entitlement for sku \(sku)", error)
) {
Task {
if let product = products[sku] {
if let result = await product.currentEntitlement {
do {
// Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
let transaction = try checkVerified(result)
resolve(serialize(transaction))
} catch StoreError.failedVerification {
reject(IapErrors.E_UNKNOWN.rawValue, "Failed to verify transaction for sku \(sku)", StoreError.failedVerification)
} catch {
debugMessage(error)
reject(IapErrors.E_UNKNOWN.rawValue, "Error fetching entitlement for sku \(sku)", error)
}
} else {
reject(IapErrors.E_DEVELOPER_ERROR.rawValue, "Can't find entitlement for sku \(sku)", nil)
}
} else {
reject(IapErrors.E_DEVELOPER_ERROR.rawValue, "Can't find entitlement for sku \(sku)", nil)
reject(IapErrors.E_DEVELOPER_ERROR.rawValue, "Can't find product for sku \(sku)", nil)
}
} else {
reject(IapErrors.E_DEVELOPER_ERROR.rawValue, "Can't find product for sku \(sku)", nil)
}
}

@objc public func latestTransaction( // TODO: new method
@objc public func latestTransaction(
_ sku: String,
resolve: @escaping RCTPromiseResolveBlock = { _ in },
reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
) async {
if let product = products[sku] {
if let result = await product.latestTransaction {
do {
// Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
let transaction = try checkVerified(result)
resolve(serialize(transaction))
} catch StoreError.failedVerification {
reject(IapErrors.E_UNKNOWN.rawValue, "Failed to verify transaction for sku \(sku)", StoreError.failedVerification)
} catch {
debugMessage(error)
reject(IapErrors.E_UNKNOWN.rawValue, "Error fetching latest transaction for sku \(sku)", error)
) {
Task {
if let product = products[sku] {
if let result = await product.latestTransaction {
do {
// Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
let transaction = try checkVerified(result)
resolve(serialize(transaction))
} catch StoreError.failedVerification {
reject(IapErrors.E_UNKNOWN.rawValue, "Failed to verify transaction for sku \(sku)", StoreError.failedVerification)
} catch {
debugMessage(error)
reject(IapErrors.E_UNKNOWN.rawValue, "Error fetching latest transaction for sku \(sku)", error)
}
} else {
reject(IapErrors.E_DEVELOPER_ERROR.rawValue, "Can't find latest transaction for sku \(sku)", nil)
}
} else {
reject(IapErrors.E_DEVELOPER_ERROR.rawValue, "Can't find latest transaction for sku \(sku)", nil)
reject(IapErrors.E_DEVELOPER_ERROR.rawValue, "Can't find product for sku \(sku)", nil)
}
} else {
reject(IapErrors.E_DEVELOPER_ERROR.rawValue, "Can't find product for sku \(sku)", nil)
}
}

Expand Down Expand Up @@ -403,15 +415,17 @@ class RNIapIosSk2: RCTEventEmitter {
resolve(transactions.values.map({(t: Transaction) in serialize(t)}))
}

// TODO: New method
@objc public func sync(
_ resolve: @escaping RCTPromiseResolveBlock = { _ in},
reject: @escaping RCTPromiseRejectBlock = {_, _, _ in}
) async {
do {
try await AppStore.sync()
} catch {
reject(IapErrors.E_SYNC_ERROR.rawValue, "Error synchronizing with the AppStore", error)
) {
Task {
do {
try await AppStore.sync()
resolve(nil)
} catch {
reject(IapErrors.E_SYNC_ERROR.rawValue, "Error synchronizing with the AppStore", error)
}
}
}

Expand Down
11 changes: 10 additions & 1 deletion src/iap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import {NativeModules, Platform} from 'react-native';
import * as IapAmazon from './modules/amazon';
import * as IapAndroid from './modules/android';
import * as IapIos from './modules/ios';
import * as IapIosSk2 from './modules/iosSk2';
import {
offerSk2Map,
ProductSk2,
productSk2Map,
subscriptionSk2Map,
} from './types/appleSk2';
import {
enableStorekit2,
fillProductsWithAdditionalData,
getAndroidModule,
getIosModule,
Expand All @@ -29,7 +31,14 @@ import {
} from './types';
import {PurchaseStateAndroid} from './types';

export {IapAndroid, IapAmazon, IapIos, isIosStorekit2};
export {
IapAndroid,
IapAmazon,
IapIos,
IapIosSk2,
isIosStorekit2,
enableStorekit2,
};

const {RNIapIos, RNIapIosSk2, RNIapModule, RNIapAmazonModule} = NativeModules;
const ANDROID_ITEM_TYPE_SUBSCRIPTION = ProductType.subs;
Expand Down
46 changes: 45 additions & 1 deletion src/modules/iosSk2.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import {NativeModules} from 'react-native';

import type {Product, ProductPurchase, Purchase, Sku} from '../types';
import type {PaymentDiscountSk2, ProductSk2} from '../types/appleSk2';
import type {
PaymentDiscountSk2,
ProductSk2,
ProductStatus,
TransactionSk2,
} from '../types/appleSk2';

import type {NativeModuleProps} from './common';
const {RNIapIosSk2} = NativeModules;

type getItems = (skus: Sku[]) => Promise<ProductSk2[]>;

Expand All @@ -27,6 +35,11 @@ type getPendingTransactions = () => Promise<ProductPurchase[]>;
type presentCodeRedemptionSheet = () => Promise<null>;

export interface IosModulePropsSk2 extends NativeModuleProps {
latestTransaction(sku: string): Promise<TransactionSk2>;
currentEntitlement(sku: string): Promise<TransactionSk2>;
subscriptionStatus(sku: string): Promise<ProductStatus[]>;
isEligibleForIntroOffer(groupID: string): Promise<Boolean>;
sync(): Promise<null>;
getItems: getItems;
getAvailableItems: getAvailableItems;
buyProduct: BuyProduct;
Expand All @@ -39,3 +52,34 @@ export interface IosModulePropsSk2 extends NativeModuleProps {
getPendingTransactions: getPendingTransactions;
presentCodeRedemptionSheet: presentCodeRedemptionSheet;
}

/**
* Sync state with Appstore (iOS only)
* https://developer.apple.com/documentation/storekit/appstore/3791906-sync
*/
export const sync = (): Promise<null> => RNIapIosSk2.sync();

/**
*
*/
export const isEligibleForIntroOffer = (groupID: string): Promise<Boolean> =>
RNIapIosSk2.isEligibleForIntroOffer(groupID);

/**
*
*/

export const subscriptionStatus = (sku: string): Promise<ProductStatus[]> =>
RNIapIosSk2.subscriptionStatus(sku);

/**
*
*/
export const currentEntitlement = (sku: string): Promise<TransactionSk2> =>
RNIapIosSk2.currentEntitlement(sku);

/**
*
*/
export const latestTransaction = (sku: string): Promise<TransactionSk2> =>
RNIapIosSk2.latestTransaction(sku);
12 changes: 12 additions & 0 deletions src/types/appleSk2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ export type TransactionSk2 = {
subscriptionGroupID: number;
webOrderLineItemID: number;
};

export type SubscriptionStatus =
| 'expired'
| 'inBillingRetryPeriod'
| 'inGracePeriod'
| 'revoked'
| 'subscribed';

export type ProductStatus = {
state: SubscriptionStatus;
};

export const transactionSk2Map = ({
id,
originalPurchaseDate,
Expand Down

0 comments on commit d62eebc

Please sign in to comment.