Giter Site home page Giter Site logo

app-store-server-library-swift's Introduction

Apple App Store Server Swift Library

The Swift server library for the App Store Server API and App Store Server Notifications. Also available in Java, Python, and Node.js.

Table of Contents

  1. Installation
  2. Documentation
  3. Usage
  4. Support

Installation

Swift Package Manager

Add the following dependency

.package(url: "https://github.com/apple/app-store-server-library-swift.git", .upToNextMinor(from: "2.2.0")),

Documentation

Documentation

WWDC Video

Obtaining an In-App Purchase key from App Store Connect

To use the App Store Server API or create promotional offer signatures, a signing key downloaded from App Store Connect is required. To obtain this key, you must have the Admin role. Go to Users and Access > Integrations > In-App Purchase. Here you can create and manage keys, as well as find your issuer ID. When using a key, you'll need the key ID and issuer ID as well.

Obtaining Apple Root Certificates

Download and store the root certificates found in the Apple Root Certificates section of the Apple PKI site. Provide these certificates as an array to a SignedDataVerifier to allow verifying the signed data comes from Apple.

Usage

API Usage

import AppStoreServerLibrary

let issuerId = "99b16628-15e4-4668-972b-eeff55eeff55"
let keyId = "ABCDEFGHIJ"
let bundleId = "com.example"
let encodedKey = try! String(contentsOfFile: "/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8")
let environment = Environment.sandbox

// try! used for example purposes only
let client = try! AppStoreServerAPIClient(signingKey: encodedKey, keyId: keyId, issuerId: issuerId, bundleId: bundleId, environment: environment)

let response = await client.requestTestNotification()
switch response {
case .success(let response):
    print(response.testNotificationToken)
case .failure(let errorCode, let rawApiError, let apiError, let errorMessage, let causedBy):
    print(errorCode)
    print(rawApiError)
    print(apiError)
    print(errorMessage)
    print(causedBy)
}

Verification Usage

import AppStoreServerLibrary

let bundleId = "com.example"
let appleRootCAs = loadRootCAs() // Specific implementation may vary
let appAppleId: Int64? = nil // appAppleId must be provided for the Production environment
let enableOnlineChecks = true
let environment = Environment.sandbox

// try! used for example purposes only
let verifier = try! SignedDataVerifier(rootCertificates: appleRootCAs, bundleId: bundleId, appAppleId: appAppleId, environment: environment, enableOnlineChecks: enableOnlineChecks)

let notificationPayload = "ey..."
let notificationResult = await verifier.verifyAndDecodeNotification(signedPayload: notificationPayload)
switch notificationResult {
case .valid(let decodedNotificaiton):
    ...
case .invalid(let error):
    ...
}

Receipt Usage

import AppStoreServerLibrary

let issuerId = "99b16628-15e4-4668-972b-eeff55eeff55"
let keyId = "ABCDEFGHIJ"
let bundleId = "com.example"
let encodedKey = try! String(contentsOfFile: "/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8")
let environment = Environment.sandbox

// try! used for example purposes only
let client = try! AppStoreServerAPIClient(signingKey: encodedKey, keyId: keyId, issuerId: issuerId, bundleId: bundleId, environment: environment)

let appReceipt = "MI..."
let transactionIdOptional = ReceiptUtility.extractTransactionId(appReceipt: appReceipt)
if let transactionId = transactionIdOptional {
    var transactionHistoryRequest = TransactionHistoryRequest()
    transactionHistoryRequest.sort = TransactionHistoryRequest.Order.ascending
    transactionHistoryRequest.revoked = false
    transactionHistoryRequest.productTypes = [TransactionHistoryRequest.ProductType.autoRenewable]

    var response: HistoryResponse?
    var transactions: [String] = []
    repeat {
        let revisionToken = response?.revision
        let apiResponse = await client.getTransactionHistory(transactionId: transactionId, revision: revisionToken, transactionHistoryRequest: transactionHistoryRequest, version: .v2)
        switch apiResponse {
        case .success(let successfulResponse):
            response = successfulResponse
        case .failure:
            // Handle Failure
            throw
        }
        if let signedTransactions = response?.signedTransactions {
            transactions.append(contentsOf: signedTransactions)
        }
    } while (response?.hasMore ?? false)
    print(transactions)
}

Promotional Offer Signature Creation

import AppStoreServerLibrary

let keyId = "ABCDEFGHIJ"
let bundleId = "com.example"
let encodedKey = try! String(contentsOfFile: "/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8")

let productId = "<product_id>"
let subscriptionOfferId = "<subscription_offer_id>"
let applicationUsername = "<application_username>"

// try! used for example purposes only
let signatureCreator = try! PromotionalOfferSignatureCreator(privateKey: encodedKey, keyId: keyId, bundleId: bundleId)

let nonce = UUID()
let timestamp = Int64(Date().timeIntervalSince1970) * 1000
let signature = signatureCreator.createSignature(productIdentifier: productIdentifier, subscriptionOfferID: subscriptionOfferID, applicationUsername: applicationUsername, nonce: nonce, timestamp: timestamp)
print(signature)

Support

Only the latest major version of the library will receive updates, including security updates. Therefore, it is recommended to update to new major versions.

app-store-server-library-swift's People

Contributors

0xtim avatar alexanderjordanbaker avatar fidetro avatar izanger avatar michalsrutek avatar mika1315 avatar nik3212 avatar philmodin avatar shimastripe avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

app-store-server-library-swift's Issues

minmum version issue

The package product 'X509' requires minimum platform version 13.0 for the iOS platform, but this target supports 12.0

appAppleId is required for verification in production environment but it's easy to miss

WHY

  • appAppleId is nil in readme sample code.
// try! used for example purposes only
let verifier = try! SignedDataVerifier(rootCertificates: appleRootCAs, bundleId: bundleId, appAppleId: nil, environment: environment, enableOnlineChecks: enableOnlineChecks)
  • This code works fine in the sandbox environment, but not in the production environment.
    • Because SignedDataVerifier is checked appAppleId in production environment.
  • if self.bundleId != bundleId || (self.environment == .production && self.appAppleId != appAppleId) {
    return VerificationResult.invalid(VerificationError.INVALID_APP_IDENTIFIER)
  • It's easy to miss, so it is better to include some measures.

How

  • Add a comment to sample code
  • Print warning logs if appAppleId is nil
  • Create a function to check the production configuration

What do you think? I would be happy to receive your comments.

Thanks!

Hope to add support to the new fields of JWSTransactionDecodedPayload added in App Store Server API 1.10

First of all, thanks to great package which saves me a lot of time!

Background

In 10/2023, Apple release App Store Server API 1.10 version, which adds following fields in JWSTransactionDecodedPayload

  • currency: The three-letter ISO 4217 currency code associated with the price parameter. This value is present only if price is present.
  • offerDiscountType: The payment mode you configure for the subscription offer, such as Free Trial, Pay As You Go, or Pay Up Front.
  • price: An integer value that represents the price multiplied by 1000 of the in-app purchase or subscription offer you configured in App Store Connect and that the system records at the time of the purchase. For more information, see price. The currency parameter indicates the currency of this price.

As for me, currency and price is so important that we cannot figure out how much user actually paid without them.
Besides, offerDiscountType is also important for we to track user subscription activity.

Hopes

I wish that we can support the fields above published in App Store Server API 1.10 as soon as possible
If we need to support above features, the JWSTransactionDecodedPayload model in our code may be needed to update to the lastest as the apple documentation Apple JWSTransactionDecodedPayload shows

Optional(-65563: ServiceNotRunning)

func testAPI() async {
        let issuerId = "f79fc525-1cdc-4dbd-a029-xxxx"
        let keyId = "BX34L8KK76"
        let bundleId = "com.ios.demo.test"
        guard let bundlePath = Bundle.main.path(forResource: "AuthKey_ BX34L8KK76", ofType: "p8") else {
            fatalError("AuthKey_ BX34L8KK76.p8 not found in the bundle.")
        }

        let encodedKey = try! String(contentsOfFile: bundlePath)
        let environment = Environment.production

        print("TEST START")
        // try! used for example purposes only
        let client = try! AppStoreServerAPIClient(signingKey: encodedKey, keyId: keyId, issuerId: issuerId, bundleId: bundleId, environment: environment)

        let response = await client.requestTestNotification()
        switch response {
        case .success(let response):
            print("TEST SUC:\(response)")
        case .failure(let errorCode, let rawApiError, let apiError, let errorMessage, let causedBy):
            print("TEST FAIL:\(errorCode)")
            print("TEST FAIL:\(rawApiError)")
            print("TEST FAIL:\(apiError)")
            print("TEST FAIL:\(errorMessage)")
            print("TEST FAIL:\(causedBy)")
        }
    }

ERROR:

TEST FAIL:nil

TEST FAIL:nil

TEST FAIL:nil

TEST FAIL:nil

TEST FAIL:Optional(-65563: ServiceNotRunning)

Consider renaming `Data` for the next major version?

First of all, thanks for making this available as a kicking off point for interacting with the App Store server APIs. I was genuinely excited to start using it!

Unfortunately, within seconds of adding this library to my Vapor project, I was immediately met with some new errors: 'Data' is ambiguous for type lookup in this context:

image

This is unfortunate, as lots of code that uses this will likely be working with Foundation as well, if only for their Codable Request/Response types, leading to the very silly spelling of Foundation.Data (which most would probably wonder why it was even necessary). Could we consider renaming it to AppMetadata or keep it under an AppStore namespace (ie. AppStore.Data)?

Similarly, but less egregious, was a conflict with Vapor's Environment, another package that has an extreme likelihood of being used with this one given its popularity for server-based swift implementations.

This isn't blocking in anyway given I could rename the conflicts with the module name prefix, but it certainly added some friction to the process of getting started, so hoping we could decrease that wherever possible 😊

ReceiptUtility.extractTransactionId(appReceipt:) unexpectedly returns nil

This is a follow up of issue #33. I filed feedback FB14087679 with examples of receipts that unexpectedly return nil.

The key issue is that ReceiptUtility.extractTransactionId(appReceipt:) doesn't throw errors, so issues are impossible to debug. The issue in #33 turned out to be a Xcode receipt, which ReceiptUtility.extractTransactionId(appReceipt:) doesn't support. It would have been convenient if the method would throw a specific error for this specific issue.
The issue I'm having now is that some (but not all) TestFlight users and the App Store Review Team seem to send receipts that either can't be parsed by ReceiptUtility.extractTransactionId(appReceipt:) or are being parsed, but don't contain an app receipt.

It's unclear to me how to debug this issue.

The client code fetched the receipts using the code provided by Apple in the documentation:

if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
           FileManager.default.fileExists(atPath: appStoreReceiptURL.path),
           let receiptData = try? Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) {
            body = receiptData.base64EncodedString(options: [])
            let receipt = String(data: receiptData, encoding: .utf8) ?? "NO RECEIPT"
}

Inconsistency between iOS and macOS in App Store Receipt Handling within extractTransactionIdFromInAppReceipt(inAppReceiptContent:)

I'm experiencing an issue where the handling of App Store Receipts seems to differ between iOS and macOS. This could be due to an error in my implementation, or it might be a bug in the AppStore Server Library.

I have an application that runs on both iOS and macOS. In the sandbox TestFlight environment, I fetch the App Store receipt and pass it to the server in the following way:

if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
           FileManager.default.fileExists(atPath: appStoreReceiptURL.path),
           let receiptData = try? Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) {
            body = receiptData.base64EncodedString(options: [])
...

On the server side, extractTransactionId(appReceipt:) is used to extract the transactionId:

guard let transactionId = ReceiptUtility.extractTransactionId(appReceipt: body) else { 
...
}
// When macOS app calls the server, transactionId is the correct transactionId
// When iOS app calls the server, transactionId is a single digit (e.g. 0)

When the macOS app communicates with the server, extractTransactionId successfully retrieves the transaction id.

However, when the iOS app communicates with the server, the receipt parsing does not function properly. More specifically, at line 75 of ReceiptUtility.swift (result = String(utf8String)), a single digit is set during each loop, and this digit can vary from loop to loop. In contrast, when the macOS app uses this method, result is set to the full tx id during each loop.

AppReceipt verification

Hello, thank you for building a great library.

I have a question: i can see ReceiptUtility only extracting transactionId without any verification and my question: if it is being sent by client, please correct me if I am wrong: an attacker can encode any transactionId in AppReceipt structure and perform a brute attack until he/she will find a transactionId that exists and will be served by the App Store Server API.

If this is correct and we are not allowed to use verifyReceipt API endpoint - how should we verify the receipt on the server? I know how to do it with Security Framework but Swift side Server can also be run on Linux as well.

Support AsyncHTTPClient

Hi! Thanks for the great package!

Have you considered adding support for using the Swift Server Workgroup’s AsyncHTTPClient for requests as an alternative to URLSession/URLRequest? That’s frequently more convenient/optimal in Swift on the server because it works nicely with SwiftNIO, where FoundationNetworking doesn’t.

Some examples:

For my use case it would make sense to just replace the implementation of AppStoreServerAPIClient entirely, but I understand that sometimes it makes sense to just use Foundation.

You can support both & even put NIO/AsyncHTTPClient support into a different target if you like. One possible solution would be to create an HTTPBackend protocol with a sendRequest(...) async throws -> ... requirement and create conforming structs like AppStoreServerURLSessionHTTPBackend and AppStoreServerNIOHTTPBackend that do the heavy lifting.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.