Skip to content

Commit

Permalink
integrate ios storekit 2 (#1882)
Browse files Browse the repository at this point in the history
* set min ios version to 15

* consolidated buy methods

* removed checks for older versions of ios

* cleared most errors

* swiftlint

* continue migration, purchases

* return promises to class

* moved utils to ios

* clean up promises and error codes

* serialized Transactions

* removed remaining old methods, added serialization

* default to Xcode 4 spaces

* Split files

* Added more transaction methods

* removed global autofinish

* fix lint on doc

Co-authored-by: Andres Aguilar <andres.aguilar@nfl.com>
  • Loading branch information
andresesfm and Andres Aguilar authored Aug 22, 2022
1 parent f26544a commit ba9482f
Show file tree
Hide file tree
Showing 13 changed files with 663 additions and 1,064 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/publish-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Publish main package

on:
release:
types: [created]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
cache: 'yarn'
registry-url: 'https://registry.npmjs.org'

- name: Remove example code
run: rm -rf IapExample

- name: Install dependencies
run: yarn install --immutable

- name: Run lint scripts
run: yarn lint:ci

- name: Verify no files have changed after auto-fix
run: git diff -- ":(exclude)IapExample/*" --exit-code HEAD

- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ opt_in_rules:
- operator_usage_whitespace
- redundant_type_annotation

indentation: 2
vertical_whitespace_closing_braces: true
vertical_whitespace_opening_braces: true
2 changes: 1 addition & 1 deletion IapExample/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

platform :ios, '12.4'
platform :ios, '15.0'
install! 'cocoapods', :deterministic_uuids => false

target 'IapExample' do
Expand Down
6 changes: 3 additions & 3 deletions IapExample/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ PODS:
- React-Core
- RNGestureHandler (2.5.0):
- React-Core
- RNIap (8.6.5):
- RNIap (9.0.0):
- React-Core
- RNScreens (3.15.0):
- React-Core
Expand Down Expand Up @@ -575,12 +575,12 @@ SPEC CHECKSUMS:
ReactCommon: e30ec17dfb1d4c4f3419eac254350d6abca6d5a2
RNCMaskedView: cb9670ea9239998340eaab21df13fa12a1f9de15
RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50
RNIap: e9f648d00e693913f80bbe5b661a5257fb72fe93
RNIap: 95084640290f4d68559dc37f1c8424b3f7dbf5d8
RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
Yoga: 7ab6e3ee4ce47d7b789d1cb520163833e515f452
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

PODFILE CHECKSUM: cfed50b11ea421296640e72457dcf62114e957f6
PODFILE CHECKSUM: 47f330ba4aa0808e88cd2a085debf346af3dc659

COCOAPODS: 1.11.3
2 changes: 1 addition & 1 deletion RNIap.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Pod::Spec.new do |s|
s.license = package["license"]
s.authors = package["author"]

s.platforms = { :ios => "10.0" , :tvos => "10.0"}
s.platforms = { :ios => "15.0" , :tvos => "15.0"}
s.source = { :git => "https://github.com/dooboolab/react-native-iap.git", :tag => "#{s.version}" }

s.source_files = "ios/*.{h,m,mm,swift}"
Expand Down
22 changes: 0 additions & 22 deletions docs/docs/usage_instructions/receipt_validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,6 @@ flow) or unstable internet connections.
For these cases we have a convenience method `getReceiptIOS()` which gets
the latest receipt for the app at any given time. The response is base64 encoded.

### iOS Purchasing process right way.

Issue regarding `valid products`

- In iOS, generally you are fetching valid products at App launching process.

If you fetch again, or fetch valid subscription, the products are added to
the array object in iOS side (Objective-C `NSMutableArray`).

This makes unexpected behavior when you fetch with a part of product lists.

For example, if you have products of `[A, B, C]`, and you call fetch function
with only `[A]`, this module returns `[A, B, C]`).

This is weird, but it works.

- But, weird result is weird, so we made a new method which remove all valid products.

If you need to clear all products, subscriptions in that array, just call
`clearProductsIOS()`, and do the fetching job again, and you will receive what
you expected.

### Example backend (Node.js)

[Here](https://github.com/mifi/in-app-subscription-example) you can find an example backend for idempotent validating of receipts on both iOS/Android and storing and serving subscription state to the client.
112 changes: 112 additions & 0 deletions ios/IapSerializationUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// IapSerializationUtils.swift
// RNIap
//
// Created by Aguilar Andres on 8/18/22.
//

import Foundation
import StoreKit

func serialize(_ p: Product) -> [String: Any?] {
return ["displayName": p.displayName,
"description": p.description,
"id": p.id,
"displayPrice": p.displayPrice,
"price": p.price,
"isFamilyShareable": p.isFamilyShareable,
"subscription": p.subscription?.subscriptionGroupID,
"jsonRepresentation": p.jsonRepresentation,
"debugDescription": p.debugDescription,
"subscription": serialize(p.subscription),
"type": serialize(p.type)
]
}

func serialize(_ e: Error?) -> [String: Any?]? {
guard let e = e else {return nil}
return ["localizedDescription": e.localizedDescription]
}

func serialize(_ si: Product.SubscriptionInfo?) -> [String: Any?]? {
guard let si = si else {return nil}
return [
"subscriptionGroupID": si.subscriptionGroupID,
"promotionalOffers": si.promotionalOffers.map {(offer: Product.SubscriptionOffer) in serialize(offer)},
"introductoryOffer": serialize(si.introductoryOffer),
"subscriptionPeriod": si.subscriptionPeriod
]
}

func serialize(_ so: Product.SubscriptionOffer?) -> [String: Any?]? {
guard let so = so else {return nil}
return [
"id": so.id,
"price": so.price,
"displayPrice": so.displayPrice,
"type": so.type,
"paymentMode": so.paymentMode,
"period": so.period,
"periodCount": so.periodCount
]
}

// Transaction
func serialize(_ t: Transaction) -> [String: Any?] {
return ["id": t.id,
"appBundleID": t.appBundleID,
"offerID": t.offerID,
"subscriptionGroupID": t.subscriptionGroupID,
"appAccountToken": t.appAccountToken,
"debugDescription": t.debugDescription,
"deviceVerification": t.deviceVerification,
"deviceVerificationNonce": t.deviceVerificationNonce,
"expirationDate": t.expirationDate,
"isUpgraded": t.isUpgraded,
"jsonRepresentation": t.jsonRepresentation,
"offerType": serialize(t.offerType),
"expirationDate": t.expirationDate,
"originalID": t.originalID,
"originalPurchaseDate": t.originalPurchaseDate,
"ownershipType": serialize(t.ownershipType),
"productType": serialize(t.productType),
"productID": t.productID,
"purchasedQuantity": t.purchasedQuantity,
"revocationDate": t.revocationDate,
"revocationReason": t.revocationReason,
"purchaseDate": t.purchaseDate,
"signedDate": t.signedDate,
"webOrderLineItemID": t.webOrderLineItemID
]
}

func serialize(_ ot: Transaction.OfferType?) -> String? {
guard let ot = ot else {return nil}
switch ot {
case .promotional: return "promotional"
case .introductory: return "introductory"
case .code: return "code"
default:
return nil
}
}
func serialize(_ ot: Transaction.OwnershipType?) -> String? {
guard let ot = ot else {return nil}
switch ot {
case .purchased: return "purchased"
case .familyShared: return "familyShared"
default:
return nil
}
}
func serialize(_ pt: Product.ProductType?) -> String? {
guard let pt = pt else {return nil}
switch pt {
case .autoRenewable: return "autoRenewable"
case .consumable: return "consumable"
case .nonConsumable: return "nonConsumable"
case .nonRenewable: return "nonRenewable"
default:
return nil
}
}
39 changes: 39 additions & 0 deletions ios/IapTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// IapTypes.swift
// RNIap
//
// Created by Aguilar Andres on 8/18/22.
//

import Foundation
import StoreKit

typealias RNIapIosPromise = (RCTPromiseResolveBlock, RCTPromiseRejectBlock)

struct ProductOrError {
let product: Product?
let error: Error?
}

public enum StoreError: Error {
case failedVerification
}

enum IapErrors: String, CaseIterable {
case E_UNKNOWN = "E_UNKNOWN"
case E_SERVICE_ERROR = "E_SERVICE_ERROR"
case E_USER_CANCELLED = "E_USER_CANCELLED"
case E_USER_ERROR = "E_USER_ERROR"
case E_ITEM_UNAVAILABLE = "E_ITEM_UNAVAILABLE"
case E_REMOTE_ERROR = "E_REMOTE_ERROR"
case E_NETWORK_ERROR = "E_NETWORK_ERROR"
case E_RECEIPT_FAILED = "E_RECEIPT_FAILED"
case E_RECEIPT_FINISHED_FAILED = "E_RECEIPT_FINISHED_FAILED"
case E_DEVELOPER_ERROR = "E_DEVELOPER_ERROR"
case E_PURCHASE_ERROR = "E_PURCHASE_ERROR"
case E_SYNC_ERROR = "E_SYNC_ERROR"
case E_DEFERRED_PAYMENT = "E_DEFERRED_PAYMENT"
func asInt() -> Int {
return IapErrors.allCases.firstIndex(of: self)!
}
}
30 changes: 30 additions & 0 deletions ios/IapUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// IapUtils.swift
// RNIap
//
// Created by Aguilar Andres on 8/15/22.
//

import Foundation
import StoreKit

public func debugMessage(_ object: Any...) {
#if DEBUG
for item in object {
print("[react-native-iap] \(item)")
}
#endif
}

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
// Check whether the JWS passes StoreKit verification.
switch result {
case .unverified:
// StoreKit parses the JWS, but it fails verification.
throw StoreError.failedVerification

case .verified(let safe):
// The result is verified. Return the unwrapped value.
return safe
}
}
47 changes: 19 additions & 28 deletions ios/RNIapIos.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,58 +12,49 @@ @interface RCT_EXTERN_MODULE (RNIapIos, NSObject)
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(getItems:
RCT_EXTERN_METHOD(products:
(NSArray*)skus
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(getAvailableItems:
RCT_EXTERN_METHOD(currentEntitlements:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(buyProduct:
RCT_EXTERN_METHOD(purchase:
(NSString*)sku
andDangerouslyFinishTransactionAutomatically:(BOOL)andDangerouslyFinishTransactionAutomatically
applicationUsername:(NSString*)applicationUsername
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(buyProductWithOffer:
(NSString*)sku
forUser:(NSString*)usernameHash
withOffer:(NSDictionary*)discountOffer
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(buyProductWithQuantityIOS:
(NSString*)sku
appAccountToken:(NSString*)appAccountToken
quantity:(NSInteger)quantity
withOffer:(NSDictionary*)discountOffer
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(clearTransaction:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(clearProducts:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(promotedProduct:
(RCTPromiseResolveBlock)resolve
RCT_EXTERN_METHOD(isEligibleForIntroOffer:
(NSString*)groupID
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(buyPromotedProduct:(RCTPromiseResolveBlock)resolve
RCT_EXTERN_METHOD(currentEntitlement:
(NSString*)sku
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(requestReceipt:
(BOOL)refresh
RCT_EXTERN_METHOD(latestTransaction:
(NSString*)sku
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(finishTransaction:
(NSString*)transactionIdentifier
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(getPendingTransactions:
RCT_EXTERN_METHOD(pendingTransactions:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(sync:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

Expand Down
Loading

0 comments on commit ba9482f

Please sign in to comment.