Changed AppStoreClient to behave more like a simple Facade (#1)
This commit is contained in:
parent
ff07f16949
commit
06092375bf
|
@ -8,51 +8,62 @@ on:
|
|||
- master
|
||||
|
||||
jobs:
|
||||
|
||||
"sanity-Tests":
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install swiftformat
|
||||
run: brew install swiftformat
|
||||
- name: Run sanity
|
||||
run: ./scripts/sanity.sh .
|
||||
|
||||
"tuxOS-Tests":
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
tag: ['5.1']
|
||||
images:
|
||||
- swift:5.1
|
||||
- swift:5.2
|
||||
- swiftlang/swift:nightly-5.3-bionic
|
||||
container:
|
||||
image: swift:${{ matrix.tag }}
|
||||
volumes:
|
||||
- $GITHUB_WORKSPACE:/src
|
||||
options: --workdir /src
|
||||
image: ${{ matrix.images }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: apt-get update && apt-get install -y zlib1g-dev zip openssl libssl-dev
|
||||
run: apt-get update && apt-get install -y curl zlib1g-dev zip openssl libssl-dev
|
||||
- name: Test
|
||||
run: swift test --enable-code-coverage --enable-test-discovery
|
||||
- name: Convert coverage files
|
||||
run: llvm-cov export -format="lcov" .build/debug/swift-app-store-receipt-validationPackageTests.xctest -instr-profile .build/debug/codecov/default.profdata > info.lcov
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v1.0.3
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{secrets.CODECOV_TOKEN}}
|
||||
file: info.lcov
|
||||
|
||||
"macOS-Tests":
|
||||
runs-on: macOS-latest
|
||||
strategy:
|
||||
matrix:
|
||||
xcode:
|
||||
- Xcode_11.1.app
|
||||
- Xcode_11.6.app
|
||||
- Xcode_12.app
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
uses: actions/checkout@v2
|
||||
- name: Show all Xcode versions
|
||||
run: ls -an /Applications/ | grep Xcode*
|
||||
- name: Change Xcode command line tools
|
||||
run: sudo xcode-select -s /Applications/Xcode_11.2.app/Contents/Developer
|
||||
- name: SPM Build
|
||||
run: swift build
|
||||
- name: SPM Tests
|
||||
run: swift test --parallel -Xswiftc -DDEBUG
|
||||
- name: Swift version
|
||||
run: swift --version
|
||||
- name: Xcode Tests
|
||||
run: |
|
||||
swift package generate-xcodeproj
|
||||
xcodebuild -quiet -parallel-testing-enabled YES -scheme swift-app-store-receipt-validation-Package -enableCodeCoverage YES build test
|
||||
- name: Codecov
|
||||
run: bash <(curl -s https://codecov.io/bash) -J 'AppStoreReceiptValidation' -t ${{secrets.CODECOV_TOKEN}}
|
||||
run: bash <(curl -s https://codecov.io/bash) -t ${{secrets.CODECOV_TOKEN}} -f *.coverage.txt
|
||||
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
.build
|
||||
/*.xcodeproj
|
||||
xcuserdata
|
||||
Package.resolved
|
|
@ -1,43 +0,0 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "async-http-client",
|
||||
"repositoryURL": "https://github.com/swift-server/async-http-client.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "51dc885a30ca704b02fa803099b0a9b5b38067b6",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "ff01888051cd7efceb1bf8319c1dd3986c4bf6fc",
|
||||
"version": "2.10.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio-extras",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio-extras.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "53808818c2015c45247cad74dc05c7a032c96a2f",
|
||||
"version": "1.3.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio-ssl",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "ccf96bbe65ecc7c1558ab0dba7ffabdea5c1d31f",
|
||||
"version": "2.4.4"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
|
@ -4,33 +4,31 @@
|
|||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "swift-app-store-receipt-validation",
|
||||
platforms: [
|
||||
.macOS(.v10_12),
|
||||
.iOS(.v10),
|
||||
.watchOS(.v3),
|
||||
.tvOS(.v10),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
.library(
|
||||
name: "AppStoreReceiptValidation",
|
||||
targets: ["AppStoreReceiptValidation"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.10.0")),
|
||||
.package(url: "https://github.com/swift-server/async-http-client.git", .upToNextMajor(from: "1.0.0"))
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
||||
.target(
|
||||
name: "AppStoreReceiptValidation",
|
||||
dependencies: ["AsyncHTTPClient", "NIO", "NIOFoundationCompat"]),
|
||||
.testTarget(
|
||||
name: "AppStoreReceiptValidationTests",
|
||||
dependencies: ["AppStoreReceiptValidation"]),
|
||||
]
|
||||
name: "swift-app-store-receipt-validation",
|
||||
platforms: [
|
||||
.macOS(.v10_12),
|
||||
.iOS(.v10),
|
||||
.watchOS(.v3),
|
||||
.tvOS(.v10),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "AppStoreReceiptValidation",
|
||||
targets: ["AppStoreReceiptValidation"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.10.0")),
|
||||
.package(url: "https://github.com/swift-server/async-http-client.git", .upToNextMajor(from: "1.0.0")),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "AppStoreReceiptValidation", dependencies: [
|
||||
.product(name: "AsyncHTTPClient", package: "async-http-client"),
|
||||
.product(name: "NIO", package: "swift-nio"),
|
||||
.product(name: "NIOFoundationCompat", package: "swift-nio"),
|
||||
]),
|
||||
.testTarget(name: "AppStoreReceiptValidationTests", dependencies: [
|
||||
.byName(name: "AppStoreReceiptValidation"),
|
||||
]),
|
||||
]
|
||||
)
|
||||
|
|
40
README.md
40
README.md
|
@ -3,8 +3,6 @@
|
|||
[](https://swift.org/download/)
|
||||
[](https://github.com/fabianfett/swift-aws-lambda/actions)
|
||||
[](https://codecov.io/gh/fabianfett/swift-app-store-receipt-validation)
|
||||

|
||||

|
||||
|
||||
This package implements the [validating receipts with the app store](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1) api.
|
||||
|
||||
|
@ -14,3 +12,41 @@ This package implements the [validating receipts with the app store](https://dev
|
|||
- [x] Automatic retry, if sandbox receipt was send to production.
|
||||
- [x] Response object is pure swift struct using enums.
|
||||
- [x] API Erros are translated into corresponding swift errors.
|
||||
|
||||
## Usage
|
||||
|
||||
Add `swift-app-store-receipt-validation`, `async-http-client` and `swift-nio` as dependencies to
|
||||
your project. For this open your `Package.swift` and add this to your dependencies:
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/swift-server/async-http-client", .upToNextMajor(from: "1.1.0")),
|
||||
.package(url: "https://github.com/apple/swift-nio", .upToNextMajor(from: "2.14.0")),
|
||||
.package(url: "https://github.com/fabianfett/swift-app-store-receipt-validation", .upToNextMajor(from: "0.1.0")),
|
||||
]
|
||||
```
|
||||
|
||||
Then, add `AsyncHTTPClient`, `SwiftNIO` and `AppStoreReceiptValidation` as target dependencies.
|
||||
|
||||
```swift
|
||||
targets: [
|
||||
.target(name: "Hello", dependencies: [
|
||||
.product(name: "NIO", package: "swift-nio"),
|
||||
.product(name: "AsyncHTTPClient", package: "async-http-client"),
|
||||
.product(name: "AppStoreReceiptValidation", package: "swift-app-store-receipt-validation"),
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
To verify an AppStore Receipt in your code you need to create an `HTTPClient` first:
|
||||
|
||||
```swift
|
||||
|
||||
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
|
||||
defer { try? httpClient.syncShutdown() }
|
||||
|
||||
let appStoreClient = AppStore.Client(httpClient: httpClient, secret: "abc123")
|
||||
|
||||
let base64EncodedReceipt: String = ...
|
||||
let receipt = try appStoreClient.validateReceipt(base64EncodedReceipt).wait()
|
||||
```
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import Foundation
|
||||
import NIO
|
||||
import NIOFoundationCompat
|
||||
import AsyncHTTPClient
|
||||
|
||||
public class AppStoreClient {
|
||||
|
||||
let httpClient: HTTPClient
|
||||
let secret : String?
|
||||
let allocator = ByteBufferAllocator()
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder() // TBD:
|
||||
|
||||
public init(eventLoopGroup: EventLoopGroup, secret: String?) {
|
||||
|
||||
self.httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup))
|
||||
self.secret = secret
|
||||
|
||||
self.decoder.dateDecodingStrategy = .custom { (decoder) -> Date in
|
||||
let container = try decoder.singleValueContainer()
|
||||
let string = try container.decode(String.self)
|
||||
|
||||
guard let timeIntervalSince1970inMs = Double(string) else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
in: container,
|
||||
debugDescription: "Expected to have a TimeInterval in ms within the string to decode a date.")
|
||||
}
|
||||
|
||||
return Date(timeIntervalSince1970: timeIntervalSince1970inMs / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
public func syncShutdown() throws {
|
||||
try self.httpClient.syncShutdown()
|
||||
}
|
||||
|
||||
public func validateAppStoreReceipt(_ receipt: String, excludeOldTransactions: Bool? = nil)
|
||||
-> EventLoopFuture<Receipt>
|
||||
{
|
||||
let request = Request(
|
||||
receiptData: receipt,
|
||||
password: secret,
|
||||
excludeOldTransactions: excludeOldTransactions)
|
||||
|
||||
return self.executeRequest(request, in: .production)
|
||||
.flatMapError { (error) -> EventLoopFuture<AppStoreClient.Response> in
|
||||
switch error {
|
||||
case Error.receiptIsFromTestEnvironmentButWasSentToProductionEnvironment:
|
||||
return self.executeRequest(request, in: .sandbox)
|
||||
default:
|
||||
// TBD: This doesn't look good. Maybe we keep the eventLoopGroup for ourselfs?
|
||||
return self.httpClient.eventLoopGroup.next().makeFailedFuture(error)
|
||||
}
|
||||
}
|
||||
.map { (response) -> (Receipt) in
|
||||
return response.receipt
|
||||
}
|
||||
}
|
||||
|
||||
private func executeRequest(
|
||||
_ request: Request,
|
||||
in environment: Environment) -> EventLoopFuture<Response>
|
||||
{
|
||||
let buffer = try! encoder.encodeAsByteBuffer(request, allocator: allocator)
|
||||
|
||||
return self.httpClient.post(url: environment.url, body: .byteBuffer(buffer), deadline: NIODeadline.now() + .seconds(5))
|
||||
.flatMapThrowing { (resp) throws -> (Response) in
|
||||
let status = try self.decoder.decode(Status.self, from: resp.body!)
|
||||
|
||||
if status.status != 0 {
|
||||
throw Error(statusCode: status.status)
|
||||
}
|
||||
|
||||
return try self.decoder.decode(Response.self, from: resp.body!)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import AsyncHTTPClient
|
||||
import class Foundation.JSONDecoder
|
||||
import class Foundation.JSONEncoder
|
||||
import NIO
|
||||
import NIOFoundationCompat
|
||||
|
||||
public protocol AppStoreClientRequestEncoder {
|
||||
func encodeAsByteBuffer<T: Encodable>(_ value: T, allocator: ByteBufferAllocator) throws -> ByteBuffer
|
||||
}
|
||||
|
||||
public protocol AppStoreClientResponseDecoder {
|
||||
func decode<T: Decodable>(_ type: T.Type, from: ByteBuffer) throws -> T
|
||||
}
|
||||
|
||||
public enum AppStore {
|
||||
public struct Client {
|
||||
let httpClient: HTTPClient
|
||||
let secret: String?
|
||||
let allocator = ByteBufferAllocator()
|
||||
let encoder: AppStoreClientRequestEncoder
|
||||
let decoder: AppStoreClientResponseDecoder
|
||||
|
||||
public init(httpClient: HTTPClient, secret: String?) {
|
||||
self.init(httpClient: httpClient,
|
||||
encoder: JSONEncoder(),
|
||||
decoder: JSONDecoder(),
|
||||
secret: secret)
|
||||
}
|
||||
|
||||
public init(
|
||||
httpClient: HTTPClient,
|
||||
encoder: AppStoreClientRequestEncoder,
|
||||
decoder: AppStoreClientResponseDecoder,
|
||||
secret: String?
|
||||
) {
|
||||
self.httpClient = httpClient
|
||||
self.encoder = encoder
|
||||
self.decoder = decoder
|
||||
self.secret = secret
|
||||
}
|
||||
|
||||
public func validateReceipt(_ receipt: String, excludeOldTransactions: Bool? = nil, allocator: ByteBufferAllocator? = nil, on eventLoop: EventLoop? = nil)
|
||||
-> EventLoopFuture<Receipt> {
|
||||
let eventLoop = eventLoop ?? self.httpClient.eventLoopGroup.next()
|
||||
let allocator = allocator ?? self.allocator
|
||||
|
||||
let request = Request(
|
||||
receiptData: receipt,
|
||||
password: self.secret,
|
||||
excludeOldTransactions: excludeOldTransactions
|
||||
)
|
||||
|
||||
return executeRequest(request, in: .production, allocator: allocator, on: eventLoop)
|
||||
.flatMapError { (error) -> EventLoopFuture<AppStore.Response> in
|
||||
switch error {
|
||||
case Error.receiptIsFromTestEnvironmentButWasSentToProductionEnvironment:
|
||||
return self.executeRequest(request, in: .sandbox, allocator: allocator, on: eventLoop)
|
||||
default:
|
||||
// TBD: This doesn't look good. Maybe we keep the eventLoopGroup for ourselfs?
|
||||
return eventLoop.makeFailedFuture(error)
|
||||
}
|
||||
}
|
||||
.map { (response) -> (Receipt) in
|
||||
response.receipt
|
||||
}
|
||||
}
|
||||
|
||||
private func executeRequest(_ request: Request, in environment: Environment, allocator: ByteBufferAllocator, on eventLoop: EventLoop)
|
||||
-> EventLoopFuture<Response> {
|
||||
return eventLoop.makeSucceededFuture(())
|
||||
.flatMapThrowing { (_) -> HTTPClient.Request in
|
||||
let buffer = try self.encoder.encodeAsByteBuffer(request, allocator: allocator)
|
||||
return try HTTPClient.Request(url: environment.url, method: .POST, body: .byteBuffer(buffer))
|
||||
}
|
||||
.flatMap { (request) -> EventLoopFuture<HTTPClient.Response> in
|
||||
self.httpClient.execute(request: request, eventLoop: .delegateAndChannel(on: eventLoop))
|
||||
}
|
||||
.flatMapThrowing { (resp) throws -> (Response) in
|
||||
let status = try self.decoder.decode(Status.self, from: resp.body!)
|
||||
|
||||
if status.status != 0 {
|
||||
throw Error(statusCode: status.status)
|
||||
}
|
||||
|
||||
return try self.decoder.decode(Response.self, from: resp.body!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONEncoder: AppStoreClientRequestEncoder {}
|
||||
|
||||
extension JSONDecoder: AppStoreClientResponseDecoder {}
|
|
@ -1,331 +1,385 @@
|
|||
import Foundation
|
||||
import struct Foundation.Date
|
||||
|
||||
extension AppStoreClient {
|
||||
|
||||
enum Environment: String, Codable {
|
||||
case sandbox = "Sandbox"
|
||||
case production = "Production"
|
||||
|
||||
var url: String {
|
||||
switch self {
|
||||
case .sandbox:
|
||||
return "https://sandbox.itunes.apple.com/verifyReceipt"
|
||||
case .production:
|
||||
return "https://buy.itunes.apple.com/verifyReceipt"
|
||||
}
|
||||
extension AppStore {
|
||||
public enum Error: Swift.Error {
|
||||
/// The App Store could not read the JSON object you provided.
|
||||
case invalidJSONObject
|
||||
|
||||
/// The data in the receipt-data property was malformed or missing.
|
||||
case receiptDataMalformedOrMissing
|
||||
|
||||
/// The receipt could not be authenticated.
|
||||
case receiptCouldNotBeAuthenticated
|
||||
|
||||
/// The shared secret you provided does not match the shared secret on file for your account.
|
||||
case sharedSecretDoesNotMatchTheSharedSecretOnFileForAccount
|
||||
|
||||
/// The receipt server is not currently available.
|
||||
case receiptServerIsCurrentlyUnavailable
|
||||
|
||||
/// This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data
|
||||
/// is also decoded and returned as part of the response.
|
||||
/// _Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions._
|
||||
case receiptIsValidButSubscriptionHasExpired
|
||||
|
||||
/// This receipt is from the test environment, but it was sent to the production environment for verification.
|
||||
/// Send it to the test environment instead.
|
||||
case receiptIsFromTestEnvironmentButWasSentToProductionEnvironment
|
||||
|
||||
/// This receipt is from the production environment, but it was sent to the test environment for verification.
|
||||
/// Send it to the production environment instead.
|
||||
case receiptIsFromProductionEnvironmentButWasSentToTestEnvironment
|
||||
|
||||
/// This receipt could not be authorized. Treat this the same as if a purchase was never made.
|
||||
case receiptCouldNotBeAuthorized
|
||||
|
||||
/// Internal data access error.
|
||||
case internalDataAccessError
|
||||
|
||||
/// Catch all error introduced by this library to handle unknown status codes
|
||||
case unknownError
|
||||
|
||||
init(statusCode: Int) {
|
||||
switch statusCode {
|
||||
case 21000:
|
||||
self = .invalidJSONObject
|
||||
case 21002:
|
||||
self = .receiptDataMalformedOrMissing
|
||||
case 21003:
|
||||
self = .receiptCouldNotBeAuthenticated
|
||||
case 21004:
|
||||
self = .sharedSecretDoesNotMatchTheSharedSecretOnFileForAccount
|
||||
case 21005:
|
||||
self = .receiptServerIsCurrentlyUnavailable
|
||||
case 21006:
|
||||
self = .receiptIsValidButSubscriptionHasExpired
|
||||
case 21007:
|
||||
self = .receiptIsFromTestEnvironmentButWasSentToProductionEnvironment
|
||||
case 21008:
|
||||
self = .receiptIsFromProductionEnvironmentButWasSentToTestEnvironment
|
||||
case 21010:
|
||||
self = .receiptCouldNotBeAuthorized
|
||||
case 21100 ... 21199:
|
||||
self = .internalDataAccessError
|
||||
default:
|
||||
self = .unknownError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Error: Swift.Error {
|
||||
/// The App Store could not read the JSON object you provided.
|
||||
case invalidJSONObject
|
||||
|
||||
/// The data in the receipt-data property was malformed or missing.
|
||||
case receiptDataMalformedOrMissing
|
||||
|
||||
/// The receipt could not be authenticated.
|
||||
case receiptCouldNotBeAuthenticated
|
||||
|
||||
/// The shared secret you provided does not match the shared secret on file for your account.
|
||||
case sharedSecretDoesNotMatchTheSharedSecretOnFileForAccount
|
||||
|
||||
/// The receipt server is not currently available.
|
||||
case receiptServerIsCurrentlyUnavailable
|
||||
|
||||
/// This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data
|
||||
/// is also decoded and returned as part of the response.
|
||||
/// _Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions._
|
||||
case receiptIsValidButSubscriptionHasExpired
|
||||
|
||||
/// This receipt is from the test environment, but it was sent to the production environment for verification.
|
||||
/// Send it to the test environment instead.
|
||||
case receiptIsFromTestEnvironmentButWasSentToProductionEnvironment
|
||||
|
||||
/// This receipt is from the production environment, but it was sent to the test environment for verification.
|
||||
/// Send it to the production environment instead.
|
||||
case receiptIsFromProductionEnvironmentButWasSentToTestEnvironment
|
||||
|
||||
/// This receipt could not be authorized. Treat this the same as if a purchase was never made.
|
||||
case receiptCouldNotBeAuthorized
|
||||
|
||||
/// Internal data access error.
|
||||
case internalDataAccessError
|
||||
|
||||
/// Catch all error introduced by this library to handle unknown status codes
|
||||
case unknownError
|
||||
|
||||
init(statusCode: Int) {
|
||||
switch statusCode {
|
||||
case 21000:
|
||||
self = .invalidJSONObject
|
||||
case 21002:
|
||||
self = .receiptDataMalformedOrMissing
|
||||
case 21003:
|
||||
self = .receiptCouldNotBeAuthenticated
|
||||
case 21004:
|
||||
self = .sharedSecretDoesNotMatchTheSharedSecretOnFileForAccount
|
||||
case 21005:
|
||||
self = .receiptServerIsCurrentlyUnavailable
|
||||
case 21006:
|
||||
self = .receiptIsValidButSubscriptionHasExpired
|
||||
case 21007:
|
||||
self = .receiptIsFromTestEnvironmentButWasSentToProductionEnvironment
|
||||
case 21008:
|
||||
self = .receiptIsFromProductionEnvironmentButWasSentToTestEnvironment
|
||||
case 21010:
|
||||
self = .receiptCouldNotBeAuthorized
|
||||
case 21100...21199:
|
||||
self = .internalDataAccessError
|
||||
default:
|
||||
self = .unknownError
|
||||
}
|
||||
|
||||
public struct Receipt: Codable {
|
||||
public let bundleId: String
|
||||
public let applicationVersion: String
|
||||
public let inApp: [InAppPurchase]
|
||||
public let originalApplicationVersion: String
|
||||
public let receiptCreationDate: Date
|
||||
public let receiptExpirationDate: Date?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case bundleId = "bundle_id"
|
||||
case applicationVersion = "application_version"
|
||||
case inApp = "in_app"
|
||||
case originalApplicationVersion = "original_application_version"
|
||||
case receiptCreationDate = "receipt_creation_date_ms"
|
||||
case receiptExpirationDate = "receipt_expiration_date_ms"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Receipt: Codable {
|
||||
public let bundleId: String
|
||||
public let applicationVersion: String
|
||||
public let inApp: [InAppPurchase]
|
||||
public let originalApplicationVersion: String
|
||||
public let receiptCreationDate: Date
|
||||
public let receiptExpirationDate: Date?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case bundleId = "bundle_id"
|
||||
case applicationVersion = "application_version"
|
||||
case inApp = "in_app"
|
||||
case originalApplicationVersion = "original_application_version"
|
||||
case receiptCreationDate = "receipt_creation_date_ms"
|
||||
case receiptExpirationDate = "receipt_expiration_date_ms"
|
||||
|
||||
public struct InAppPurchase: Codable {
|
||||
/// The number of items purchased.
|
||||
///
|
||||
/// This value corresponds to the quantity property of the SKPayment object stored in the transaction’s payment property.
|
||||
public let quantity: String
|
||||
|
||||
/// The product identifier of the item that was purchased.
|
||||
///
|
||||
/// This value corresponds to the productIdentifier property of the SKPayment object stored in the transaction’s payment property.
|
||||
public let productId: String
|
||||
|
||||
/// The transaction identifier of the item that was purchased.
|
||||
///
|
||||
/// This value corresponds to the transaction’s `transactionIdentifier` property.
|
||||
///
|
||||
/// For a transaction that restores a previous transaction, this value is different from the transaction identifier of the original
|
||||
/// purchase transaction. In an auto-renewable subscription receipt, a new value for the transaction identifier is generated every
|
||||
/// time the subscription automatically renews or is restored on a new device.
|
||||
public let transactionId: String
|
||||
|
||||
/// For a transaction that restores a previous transaction, the transaction identifier of the original transaction. Otherwise, identical
|
||||
/// to the transaction identifier.
|
||||
///
|
||||
/// This value corresponds to the original transaction’s transactionIdentifier property.
|
||||
///
|
||||
/// This value is the same for all receipts that have been generated for a specific subscription. This value is useful for relating
|
||||
/// together multiple iOS 6 style transaction receipts for the same individual customer’s subscription.
|
||||
public let originalTransactionId: String
|
||||
|
||||
/// The date and time that the item was purchased.
|
||||
///
|
||||
/// This value corresponds to the transaction’s transactionDate property.
|
||||
///
|
||||
/// For a transaction that restores a previous transaction, the purchase date is the same as the original purchase date. Use
|
||||
/// Original Purchase Date to get the date of the original transaction.
|
||||
///
|
||||
/// In an auto-renewable subscription receipt, the purchase date is the date when the subscription was either purchased or
|
||||
/// renewed (with or without a lapse). For an automatic renewal that occurs on the expiration date of the current period, the
|
||||
/// purchase date is the start date of the next period, which is identical to the end date of the current period.
|
||||
public let purchaseDate: Date
|
||||
|
||||
/// For a transaction that restores a previous transaction, the date of the original transaction.
|
||||
///
|
||||
/// This value corresponds to the original transaction’s transactionDate property.
|
||||
///
|
||||
/// In an auto-renewable subscription receipt, this indicates the beginning of the subscription period, even if the subscription
|
||||
/// has been renewed.
|
||||
public let originalPurchaseDate: Date
|
||||
|
||||
/// The expiration date for the subscription, expressed as the number of milliseconds since January 1, 1970, 00:00:00 GMT.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts. Use this value to identify the date when the subscription
|
||||
/// will renew or expire, to determine if a customer should have access to content or service. After validating the latest receipt,
|
||||
/// if the subscription expiration date for the latest renewal transaction is a past date, it is safe to assume that the subscription
|
||||
/// has expired.
|
||||
public let subscriptionExpirationDate: Date?
|
||||
|
||||
/// For an expired subscription, the reason for the subscription expiration.
|
||||
///
|
||||
/// - 1 - Customer canceled their subscription.
|
||||
/// - 2 - Billing error; for example customer’s payment information was no longer valid.
|
||||
/// - 3 - Customer did not agree to a recent price increase.
|
||||
/// - 4 - Product was not available for purchase at the time of renewal.
|
||||
/// - 5 - Unknown error.
|
||||
///
|
||||
/// This key is only present for a receipt containing an expired auto-renewable subscription. You can use this value to decide
|
||||
/// whether to display appropriate messaging in your app for customers to resubscribe.
|
||||
public let subscriptionExpirationIntent: SubscriptionExpirationIntent?
|
||||
|
||||
/// For an expired subscription, whether or not Apple is still attempting to automatically renew the subscription.
|
||||
///
|
||||
/// - 1 - App Store is still attempting to renew the subscription.
|
||||
/// - 0 - App Store has stopped attempting to renew the subscription.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts. If the customer’s subscription failed to renew because
|
||||
/// the App Store was unable to complete the transaction, this value will reflect whether or not the App Store is still trying to
|
||||
/// renew the subscription.
|
||||
public let subscriptionRetryFlag: SubscriptionRetryFlag?
|
||||
|
||||
/// For a subscription, whether or not it is in the free trial period.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts. The value for this key is "true" if the customer’s
|
||||
/// subscription is currently in the free trial period, or "false" if not.
|
||||
///
|
||||
/// **Note**: If a previous subscription period in the receipt has the value “true” for either the `is_trial_period` or the
|
||||
/// `is_in_intro_offer_period` key, the user is not eligible for a free trial or introductory price within that
|
||||
/// subscription group.
|
||||
public let subscriptionTrialPeriod: SubscriptionTrialPeriod?
|
||||
|
||||
/// For an auto-renewable subscription, whether or not it is in the introductory price period.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts. The value for this key is "true" if the customer’s
|
||||
/// subscription is currently in an introductory price period, or "false" if not.
|
||||
///
|
||||
/// **Note**: If a previous subscription period in the receipt has the value “true” for either the `is_trial_period` or the
|
||||
/// `is_in_intro_offer_period` key, the user is not eligible for a free trial or introductory price within that
|
||||
/// subscription group.
|
||||
public let subscriptionIsInIntroductoryPricePeriod: SubscriptionIntroductoryPricePeriod?
|
||||
|
||||
/// The current renewal status for the auto-renewable subscription.
|
||||
///
|
||||
/// - 1 - Subscription will renew at the end of the current subscription period.
|
||||
/// - 0 - Customer has turned off automatic renewal for their subscription.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts, for active or expired subscriptions. The value for
|
||||
/// this key should not be interpreted as the customer’s subscription status. You can use this value to display an
|
||||
/// alternative subscription product in your app, for example, a lower level subscription plan that the customer can
|
||||
/// downgrade to from their current plan.
|
||||
public let subscriptionAutoRenewStatus: SubscriptionAutoRenewStatus?
|
||||
|
||||
/// The current renewal preference for the auto-renewable subscription.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts. The value for this key corresponds to the
|
||||
/// `productIdentifier` property of the product that the customer’s subscription renews. You can use this value to
|
||||
/// present an alternative service level to the customer before the current subscription period ends.
|
||||
public let subscriptionAutoRenewPreface: String?
|
||||
|
||||
/// The current price consent status for a subscription price increase.
|
||||
///
|
||||
/// - 1 - Customer has agreed to the price increase. Subscription will renew at the higher price.
|
||||
/// - 0 - Customer has not taken action regarding the increased price. Subscription expires if the customer
|
||||
/// takes no action before the renewal date.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts if the subscription price was
|
||||
/// increased without keeping the existing price for active subscribers. You can use this value to
|
||||
/// track customer adoption of the new price and take appropriate action.
|
||||
public let subscriptionPriceConsentStatus: SubscriptionPriceConsentStatus?
|
||||
|
||||
/// For a transaction that was canceled by Apple customer support, the time and date of the cancellation. For an
|
||||
/// auto-renewable subscription plan that was upgraded, the time and date of the upgrade transaction.
|
||||
///
|
||||
/// Treat a canceled receipt the same as if no purchase had ever been made.
|
||||
///
|
||||
/// **Note**: A canceled in-app purchase remains in the receipt indefinitely. Only applicable if the refund was for a
|
||||
/// non-consumable product, an auto-renewable subscription, a non-renewing subscription, or for a
|
||||
/// free subscription.
|
||||
public let cancellationDate: Date?
|
||||
|
||||
/// For a transaction that was canceled, the reason for cancellation.
|
||||
///
|
||||
/// - 1 - Customer canceled their transaction due to an actual or perceived issue within your app.
|
||||
/// - 0 - Transaction was canceled for another reason, for example, if the customer made the purchase accidentally.
|
||||
///
|
||||
/// Use this value along with the cancellation date to identify possible issues in your app that may lead customers to
|
||||
/// contact Apple customer support.
|
||||
public let cancellationReason: CancellationReason?
|
||||
|
||||
/// A string that the App Store uses to uniquely identify the application that created the transaction.
|
||||
///
|
||||
/// If your server supports multiple applications, you can use this value to differentiate between them. Apps are assigned an
|
||||
/// identifier only in the production environment, so this key is not present for receipts created in the test environment.
|
||||
///
|
||||
/// This field is not present for Mac apps.
|
||||
public let appItemId: String?
|
||||
|
||||
/// An arbitrary number that uniquely identifies a revision of your application.
|
||||
///
|
||||
/// This key is not present for receipts created in the test environment. Use this value to identify the version of the app that
|
||||
/// the customer bought.
|
||||
public let externalVersionIdentifier: String?
|
||||
|
||||
/// The primary key for identifying subscription purchases.
|
||||
///
|
||||
/// This value is a unique ID that identifies purchase events across devices, including subscription renewal purchase events.
|
||||
public let webOrderLineItemId: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case quantity
|
||||
case productId = "product_id"
|
||||
case transactionId = "transaction_id"
|
||||
case originalTransactionId = "original_transaction_id"
|
||||
case purchaseDate = "purchase_date_ms"
|
||||
case originalPurchaseDate = "original_purchase_date_ms"
|
||||
case subscriptionExpirationDate = "subscription_expiration_date_ms"
|
||||
case subscriptionExpirationIntent = "expiration_intent"
|
||||
case subscriptionRetryFlag = "is_in_billing_retry_period"
|
||||
case subscriptionTrialPeriod = "is_trial_period"
|
||||
case subscriptionIsInIntroductoryPricePeriod = "is_in_intro_offer_period"
|
||||
case subscriptionAutoRenewStatus = "auto_renew_status"
|
||||
case subscriptionAutoRenewPreface = "auto_renew_product_id"
|
||||
case subscriptionPriceConsentStatus = "price_consent_status"
|
||||
case cancellationDate = "cancellationDateMS"
|
||||
case cancellationReason = "cancellation_reason"
|
||||
case appItemId = "app_item_id"
|
||||
case externalVersionIdentifier = "version_external_identifier"
|
||||
case webOrderLineItemId = "web_order_line_item_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct InAppPurchase: Codable {
|
||||
|
||||
/// The number of items purchased.
|
||||
///
|
||||
/// This value corresponds to the quantity property of the SKPayment object stored in the transaction’s payment property.
|
||||
public let quantity : String
|
||||
|
||||
/// The product identifier of the item that was purchased.
|
||||
///
|
||||
/// This value corresponds to the productIdentifier property of the SKPayment object stored in the transaction’s payment property.
|
||||
public let productId : String
|
||||
|
||||
/// The transaction identifier of the item that was purchased.
|
||||
///
|
||||
/// This value corresponds to the transaction’s `transactionIdentifier` property.
|
||||
///
|
||||
/// For a transaction that restores a previous transaction, this value is different from the transaction identifier of the original
|
||||
/// purchase transaction. In an auto-renewable subscription receipt, a new value for the transaction identifier is generated every
|
||||
/// time the subscription automatically renews or is restored on a new device.
|
||||
public let transactionId : String
|
||||
|
||||
/// For a transaction that restores a previous transaction, the transaction identifier of the original transaction. Otherwise, identical
|
||||
/// to the transaction identifier.
|
||||
///
|
||||
/// This value corresponds to the original transaction’s transactionIdentifier property.
|
||||
///
|
||||
/// This value is the same for all receipts that have been generated for a specific subscription. This value is useful for relating
|
||||
/// together multiple iOS 6 style transaction receipts for the same individual customer’s subscription.
|
||||
public let originalTransactionId : String
|
||||
|
||||
/// The date and time that the item was purchased.
|
||||
///
|
||||
/// This value corresponds to the transaction’s transactionDate property.
|
||||
///
|
||||
/// For a transaction that restores a previous transaction, the purchase date is the same as the original purchase date. Use
|
||||
/// Original Purchase Date to get the date of the original transaction.
|
||||
///
|
||||
/// In an auto-renewable subscription receipt, the purchase date is the date when the subscription was either purchased or
|
||||
/// renewed (with or without a lapse). For an automatic renewal that occurs on the expiration date of the current period, the
|
||||
/// purchase date is the start date of the next period, which is identical to the end date of the current period.
|
||||
public let purchaseDate : Date
|
||||
|
||||
/// For a transaction that restores a previous transaction, the date of the original transaction.
|
||||
///
|
||||
/// This value corresponds to the original transaction’s transactionDate property.
|
||||
///
|
||||
/// In an auto-renewable subscription receipt, this indicates the beginning of the subscription period, even if the subscription
|
||||
/// has been renewed.
|
||||
public let originalPurchaseDate : Date
|
||||
|
||||
/// The expiration date for the subscription, expressed as the number of milliseconds since January 1, 1970, 00:00:00 GMT.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts. Use this value to identify the date when the subscription
|
||||
/// will renew or expire, to determine if a customer should have access to content or service. After validating the latest receipt,
|
||||
/// if the subscription expiration date for the latest renewal transaction is a past date, it is safe to assume that the subscription
|
||||
/// has expired.
|
||||
public let subscriptionExpirationDate : Date?
|
||||
|
||||
/// For an expired subscription, the reason for the subscription expiration.
|
||||
///
|
||||
/// - 1 - Customer canceled their subscription.
|
||||
/// - 2 - Billing error; for example customer’s payment information was no longer valid.
|
||||
/// - 3 - Customer did not agree to a recent price increase.
|
||||
/// - 4 - Product was not available for purchase at the time of renewal.
|
||||
/// - 5 - Unknown error.
|
||||
///
|
||||
/// This key is only present for a receipt containing an expired auto-renewable subscription. You can use this value to decide
|
||||
/// whether to display appropriate messaging in your app for customers to resubscribe.
|
||||
public let subscriptionExpirationIntent : SubscriptionExpirationIntent?
|
||||
|
||||
/// For an expired subscription, whether or not Apple is still attempting to automatically renew the subscription.
|
||||
///
|
||||
/// - 1 - App Store is still attempting to renew the subscription.
|
||||
/// - 0 - App Store has stopped attempting to renew the subscription.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts. If the customer’s subscription failed to renew because
|
||||
/// the App Store was unable to complete the transaction, this value will reflect whether or not the App Store is still trying to
|
||||
/// renew the subscription.
|
||||
public let subscriptionRetryFlag : SubscriptionRetryFlag?
|
||||
|
||||
/// For a subscription, whether or not it is in the free trial period.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts. The value for this key is "true" if the customer’s
|
||||
/// subscription is currently in the free trial period, or "false" if not.
|
||||
///
|
||||
/// **Note**: If a previous subscription period in the receipt has the value “true” for either the `is_trial_period` or the
|
||||
/// `is_in_intro_offer_period` key, the user is not eligible for a free trial or introductory price within that
|
||||
/// subscription group.
|
||||
public let subscriptionTrialPeriod : SubscriptionTrialPeriod?
|
||||
|
||||
/// For an auto-renewable subscription, whether or not it is in the introductory price period.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts. The value for this key is "true" if the customer’s
|
||||
/// subscription is currently in an introductory price period, or "false" if not.
|
||||
///
|
||||
/// **Note**: If a previous subscription period in the receipt has the value “true” for either the `is_trial_period` or the
|
||||
/// `is_in_intro_offer_period` key, the user is not eligible for a free trial or introductory price within that
|
||||
/// subscription group.
|
||||
public let subscriptionIsInIntroductoryPricePeriod : SubscriptionIntroductoryPricePeriod?
|
||||
|
||||
/// The current renewal status for the auto-renewable subscription.
|
||||
///
|
||||
/// - 1 - Subscription will renew at the end of the current subscription period.
|
||||
/// - 0 - Customer has turned off automatic renewal for their subscription.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts, for active or expired subscriptions. The value for
|
||||
/// this key should not be interpreted as the customer’s subscription status. You can use this value to display an
|
||||
/// alternative subscription product in your app, for example, a lower level subscription plan that the customer can
|
||||
/// downgrade to from their current plan.
|
||||
public let subscriptionAutoRenewStatus : SubscriptionAutoRenewStatus?
|
||||
|
||||
/// The current renewal preference for the auto-renewable subscription.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts. The value for this key corresponds to the
|
||||
/// `productIdentifier` property of the product that the customer’s subscription renews. You can use this value to
|
||||
/// present an alternative service level to the customer before the current subscription period ends.
|
||||
public let subscriptionAutoRenewPreface : String?
|
||||
|
||||
/// The current price consent status for a subscription price increase.
|
||||
///
|
||||
/// - 1 - Customer has agreed to the price increase. Subscription will renew at the higher price.
|
||||
/// - 0 - Customer has not taken action regarding the increased price. Subscription expires if the customer
|
||||
/// takes no action before the renewal date.
|
||||
///
|
||||
/// This key is only present for auto-renewable subscription receipts if the subscription price was
|
||||
/// increased without keeping the existing price for active subscribers. You can use this value to
|
||||
/// track customer adoption of the new price and take appropriate action.
|
||||
public let subscriptionPriceConsentStatus : SubscriptionPriceConsentStatus?
|
||||
|
||||
/// For a transaction that was canceled by Apple customer support, the time and date of the cancellation. For an
|
||||
/// auto-renewable subscription plan that was upgraded, the time and date of the upgrade transaction.
|
||||
///
|
||||
/// Treat a canceled receipt the same as if no purchase had ever been made.
|
||||
///
|
||||
/// **Note**: A canceled in-app purchase remains in the receipt indefinitely. Only applicable if the refund was for a
|
||||
/// non-consumable product, an auto-renewable subscription, a non-renewing subscription, or for a
|
||||
/// free subscription.
|
||||
public let cancellationDate : Date?
|
||||
|
||||
/// For a transaction that was canceled, the reason for cancellation.
|
||||
///
|
||||
/// - 1 - Customer canceled their transaction due to an actual or perceived issue within your app.
|
||||
/// - 0 - Transaction was canceled for another reason, for example, if the customer made the purchase accidentally.
|
||||
///
|
||||
/// Use this value along with the cancellation date to identify possible issues in your app that may lead customers to
|
||||
/// contact Apple customer support.
|
||||
public let cancellationReason : CancellationReason?
|
||||
|
||||
/// A string that the App Store uses to uniquely identify the application that created the transaction.
|
||||
///
|
||||
/// If your server supports multiple applications, you can use this value to differentiate between them. Apps are assigned an
|
||||
/// identifier only in the production environment, so this key is not present for receipts created in the test environment.
|
||||
///
|
||||
/// This field is not present for Mac apps.
|
||||
public let appItemId : String?
|
||||
|
||||
/// An arbitrary number that uniquely identifies a revision of your application.
|
||||
///
|
||||
/// This key is not present for receipts created in the test environment. Use this value to identify the version of the app that
|
||||
/// the customer bought.
|
||||
public let externalVersionIdentifier : String?
|
||||
|
||||
/// The primary key for identifying subscription purchases.
|
||||
///
|
||||
/// This value is a unique ID that identifies purchase events across devices, including subscription renewal purchase events.
|
||||
public let webOrderLineItemId : String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case quantity = "quantity"
|
||||
case productId = "product_id"
|
||||
case transactionId = "transaction_id"
|
||||
case originalTransactionId = "original_transaction_id"
|
||||
case purchaseDate = "purchase_date_ms"
|
||||
case originalPurchaseDate = "original_purchase_date_ms"
|
||||
case subscriptionExpirationDate = "subscription_expiration_date_ms"
|
||||
case subscriptionExpirationIntent = "expiration_intent"
|
||||
case subscriptionRetryFlag = "is_in_billing_retry_period"
|
||||
case subscriptionTrialPeriod = "is_trial_period"
|
||||
case subscriptionIsInIntroductoryPricePeriod = "is_in_intro_offer_period"
|
||||
case subscriptionAutoRenewStatus = "auto_renew_status"
|
||||
case subscriptionAutoRenewPreface = "auto_renew_product_id"
|
||||
case subscriptionPriceConsentStatus = "price_consent_status"
|
||||
case cancellationDate = "cancellationDateMS"
|
||||
case cancellationReason = "cancellation_reason"
|
||||
case appItemId = "app_item_id"
|
||||
case externalVersionIdentifier = "version_external_identifier"
|
||||
case webOrderLineItemId = "web_order_line_item_id"
|
||||
|
||||
public enum SubscriptionExpirationIntent: String, Codable {
|
||||
case customerCancelled = "1"
|
||||
case billingError = "2"
|
||||
case customerDidNotAgreeWithPriceIncrease = "3"
|
||||
case productWasUnavailableAtTimeOfRenewal = "4"
|
||||
case unknownError = "5"
|
||||
}
|
||||
|
||||
public enum SubscriptionRetryFlag: String, Codable {
|
||||
case appStoreStillAttemptingToRenewSubscription = "0"
|
||||
case appStoreHasStoppedAttemptingToRenewTheSubscription = "1"
|
||||
}
|
||||
|
||||
public enum SubscriptionTrialPeriod: String, Codable {
|
||||
case isInFreeTrial = "true"
|
||||
case isNotInFreeTrial = "false"
|
||||
}
|
||||
|
||||
public enum SubscriptionIntroductoryPricePeriod: String, Codable {
|
||||
case isInIntroductoryPricePeriod = "true"
|
||||
case isNotInIntroductoryPricePeriod = "false"
|
||||
}
|
||||
|
||||
public enum SubscriptionAutoRenewStatus: String, Codable {
|
||||
case customerHasTurnedOffAutomaticRenewal = "0"
|
||||
case willRenew = "1"
|
||||
}
|
||||
|
||||
public enum SubscriptionPriceConsentStatus: String, Codable {
|
||||
case customerHasNotTakenAction = "0"
|
||||
case customerHasAgreedToPriceIncrease = "1"
|
||||
}
|
||||
|
||||
public enum CancellationReason: String, Codable {
|
||||
case actualOrPercivedIssueWithinTheApp = "1"
|
||||
case otherReason = "0"
|
||||
}
|
||||
}
|
||||
|
||||
extension AppStore.Receipt {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.bundleId = try container.decode(String.self, forKey: .bundleId)
|
||||
self.applicationVersion = try container.decode(String.self, forKey: .applicationVersion)
|
||||
self.inApp = try container.decode([AppStore.InAppPurchase].self, forKey: .inApp)
|
||||
self.originalApplicationVersion = try container.decode(String.self, forKey: .originalApplicationVersion)
|
||||
self.receiptCreationDate = try container.decodeAppStoreDate(forKey: .receiptCreationDate)
|
||||
self.receiptExpirationDate = try container.decodeAppStoreDateIfPresent(forKey: .receiptExpirationDate)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppStore.InAppPurchase {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.quantity = try container.decode(String.self, forKey: .quantity)
|
||||
self.productId = try container.decode(String.self, forKey: .productId)
|
||||
self.transactionId = try container.decode(String.self, forKey: .transactionId)
|
||||
self.originalTransactionId = try container.decode(String.self, forKey: .originalTransactionId)
|
||||
self.purchaseDate = try container.decodeAppStoreDate(forKey: .purchaseDate)
|
||||
self.originalPurchaseDate = try container.decodeAppStoreDate(forKey: .originalPurchaseDate)
|
||||
self.subscriptionExpirationDate = try container.decodeAppStoreDateIfPresent(forKey: .subscriptionExpirationDate)
|
||||
self.subscriptionExpirationIntent = try container.decodeIfPresent(AppStore.SubscriptionExpirationIntent.self, forKey: .subscriptionExpirationIntent)
|
||||
self.subscriptionRetryFlag = try container.decodeIfPresent(AppStore.SubscriptionRetryFlag.self, forKey: .subscriptionRetryFlag)
|
||||
self.subscriptionTrialPeriod = try container.decodeIfPresent(AppStore.SubscriptionTrialPeriod.self, forKey: .subscriptionTrialPeriod)
|
||||
self.subscriptionIsInIntroductoryPricePeriod = try container.decodeIfPresent(AppStore.SubscriptionIntroductoryPricePeriod.self, forKey: .subscriptionIsInIntroductoryPricePeriod)
|
||||
self.subscriptionAutoRenewStatus = try container.decodeIfPresent(AppStore.SubscriptionAutoRenewStatus.self, forKey: .subscriptionAutoRenewStatus)
|
||||
self.subscriptionAutoRenewPreface = try container.decodeIfPresent(String.self, forKey: .subscriptionAutoRenewPreface)
|
||||
self.subscriptionPriceConsentStatus = try container.decodeIfPresent(AppStore.SubscriptionPriceConsentStatus.self, forKey: .subscriptionPriceConsentStatus)
|
||||
self.cancellationDate = try container.decodeAppStoreDateIfPresent(forKey: .cancellationDate)
|
||||
self.cancellationReason = try container.decodeIfPresent(AppStore.CancellationReason.self, forKey: .cancellationReason)
|
||||
self.appItemId = try container.decodeIfPresent(String.self, forKey: .appItemId)
|
||||
self.externalVersionIdentifier = try container.decodeIfPresent(String.self, forKey: .externalVersionIdentifier)
|
||||
self.webOrderLineItemId = try container.decodeIfPresent(String.self, forKey: .webOrderLineItemId)
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyedDecodingContainer {
|
||||
func decodeAppStoreDate(forKey key: K) throws -> Date {
|
||||
let string = try self.decode(String.self, forKey: key)
|
||||
|
||||
guard let timeIntervalSince1970inMs = Double(string) else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: key,
|
||||
in: self,
|
||||
debugDescription: "Expected to have a TimeInterval in ms within the string to decode a date."
|
||||
)
|
||||
}
|
||||
|
||||
return Date(timeIntervalSince1970: timeIntervalSince1970inMs / 1000)
|
||||
}
|
||||
|
||||
func decodeAppStoreDateIfPresent(forKey key: K) throws -> Date? {
|
||||
guard let string = try self.decodeIfPresent(String.self, forKey: key) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let timeIntervalSince1970inMs = Double(string) else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: key,
|
||||
in: self,
|
||||
debugDescription: "Expected to have a TimeInterval in ms within the string to decode a date."
|
||||
)
|
||||
}
|
||||
|
||||
return Date(timeIntervalSince1970: timeIntervalSince1970inMs / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
public enum SubscriptionExpirationIntent: String, Codable {
|
||||
case customerCancelled = "1"
|
||||
case billingError = "2"
|
||||
case customerDidNotAgreeWithPriceIncrease = "3"
|
||||
case productWasUnavailableAtTimeOfRenewal = "4"
|
||||
case unknownError = "5"
|
||||
}
|
||||
|
||||
public enum SubscriptionRetryFlag: String, Codable {
|
||||
case appStoreStillAttemptingToRenewSubscription = "0"
|
||||
case appStoreHasStoppedAttemptingToRenewTheSubscription = "1"
|
||||
}
|
||||
|
||||
public enum SubscriptionTrialPeriod: String, Codable {
|
||||
case isInFreeTrial = "true"
|
||||
case isNotInFreeTrial = "false"
|
||||
}
|
||||
|
||||
public enum SubscriptionIntroductoryPricePeriod: String, Codable {
|
||||
case isInIntroductoryPricePeriod = "true"
|
||||
case isNotInIntroductoryPricePeriod = "false"
|
||||
}
|
||||
|
||||
public enum SubscriptionAutoRenewStatus: String, Codable {
|
||||
case customerHasTurnedOffAutomaticRenewal = "0"
|
||||
case willRenew = "1"
|
||||
}
|
||||
|
||||
public enum SubscriptionPriceConsentStatus: String, Codable {
|
||||
case customerHasNotTakenAction = "0"
|
||||
case customerHasAgreedToPriceIncrease = "1"
|
||||
}
|
||||
|
||||
public enum CancellationReason: String, Codable {
|
||||
case actualOrPercivedIssueWithinTheApp = "1"
|
||||
case otherReason = "0"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,48 +1,54 @@
|
|||
import Foundation
|
||||
import NIO
|
||||
|
||||
extension AppStoreClient {
|
||||
|
||||
struct Request: Codable {
|
||||
|
||||
let receiptData: String
|
||||
let password: String?
|
||||
let excludeOldTransactions: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case receiptData = "receipt-data"
|
||||
case password = "password"
|
||||
case excludeOldTransactions = "exclude-old-transactions"
|
||||
extension AppStore {
|
||||
enum Environment: String, Codable {
|
||||
case sandbox = "Sandbox"
|
||||
case production = "Production"
|
||||
|
||||
var url: String {
|
||||
switch self {
|
||||
case .sandbox:
|
||||
return "https://sandbox.itunes.apple.com/verifyReceipt"
|
||||
case .production:
|
||||
return "https://buy.itunes.apple.com/verifyReceipt"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AppStoreClient {
|
||||
|
||||
struct Status: Codable {
|
||||
let status: Int
|
||||
}
|
||||
|
||||
struct Response: Codable {
|
||||
|
||||
let receipt: Receipt // json
|
||||
let latestReceipt: String?
|
||||
let latestReceiptInfo: Receipt? // json
|
||||
extension AppStore {
|
||||
struct Request: Codable {
|
||||
let receiptData: String
|
||||
let password: String?
|
||||
let excludeOldTransactions: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case receiptData = "receipt-data"
|
||||
case password
|
||||
case excludeOldTransactions = "exclude-old-transactions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppStore {
|
||||
struct Status: Codable {
|
||||
let status: Int
|
||||
}
|
||||
|
||||
struct Response: Codable {
|
||||
let receipt: Receipt // json
|
||||
let latestReceipt: String?
|
||||
let latestReceiptInfo: Receipt? // json
|
||||
// let latestExpiredReceiptInfo: Any? // json
|
||||
// let pendingRenewalInfo: Any?
|
||||
let isRetryable: Bool?
|
||||
let environment: Environment
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case receipt = "receipt"
|
||||
case latestReceipt = "latest_receipt"
|
||||
case latestReceiptInfo = "latest_receipt_info"
|
||||
case isRetryable = "is-retryable"
|
||||
case environment = "environment"
|
||||
}
|
||||
let isRetryable: Bool?
|
||||
let environment: Environment
|
||||
|
||||
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case receipt
|
||||
case latestReceipt = "latest_receipt"
|
||||
case latestReceiptInfo = "latest_receipt_info"
|
||||
case isRetryable = "is-retryable"
|
||||
case environment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
@testable import AppStoreReceiptValidation
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import AppStoreReceiptValidation
|
||||
|
||||
class AppStoreClientTests: XCTestCase {
|
||||
|
||||
func testNothing() {
|
||||
|
||||
}
|
||||
|
||||
func testNothing() {}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
printf "=> Checking format... "
|
||||
FIRST_OUT="$(git status --porcelain)"
|
||||
swiftformat . > /dev/null 2>&1
|
||||
SECOND_OUT="$(git status --porcelain)"
|
||||
if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then
|
||||
printf "\033[0;31mformatting issues!\033[0m\n"
|
||||
git --no-pager diff
|
||||
exit 1
|
||||
else
|
||||
printf "\033[0;32mokay.\033[0m\n"
|
||||
fi
|
Loading…
Reference in New Issue