增加apple内购相关代码
This commit is contained in:
parent
520b5ce06f
commit
a4e2cb14cd
4
.gitignore
vendored
4
.gitignore
vendored
@ -6,4 +6,6 @@ dist
|
||||
tmp
|
||||
target
|
||||
boundle.log
|
||||
google_cloud.json
|
||||
google_cloud.json
|
||||
.env.production
|
||||
.env.development
|
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
@ -30,5 +30,18 @@
|
||||
],
|
||||
"type": "pwa-node"
|
||||
},
|
||||
{
|
||||
"name": "Debug Test",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run-script",
|
||||
"test"
|
||||
],
|
||||
"runtimeExecutable": "npm",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "pwa-node"
|
||||
},
|
||||
]
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev:api": "ts-node -r tsconfig-paths/register src/api.ts",
|
||||
"test": "ts-node -r tsconfig-paths/register src/test.ts",
|
||||
"dev:monitor": "ts-node -r tsconfig-paths/register src/google.monitor.ts",
|
||||
"build": "tsc",
|
||||
"prod:api": "NODE_ENV=production NODE_PATH=./dist node dist/api.js",
|
||||
@ -28,6 +29,7 @@
|
||||
"fastify-plugin": "^4.2.1",
|
||||
"google-auth-library": "^8.5.2",
|
||||
"googleapis": "^120.0.0",
|
||||
"jose": "^4.14.4",
|
||||
"mongoose": "^6.6.5",
|
||||
"mongoose-findorcreate": "^3.0.0",
|
||||
"nanoid": "^3.1.23",
|
||||
|
233
src/apple/AppStoreServerAPI.ts
Normal file
233
src/apple/AppStoreServerAPI.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import fetch from 'node-fetch'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import * as jose from 'jose'
|
||||
import {
|
||||
CheckTestNotificationResponse,
|
||||
ConsumptionRequest,
|
||||
Environment,
|
||||
HistoryResponse,
|
||||
NotificationHistoryQuery,
|
||||
NotificationHistoryRequest,
|
||||
NotificationHistoryResponse,
|
||||
OrderLookupResponse,
|
||||
SendTestNotificationResponse,
|
||||
StatusResponse,
|
||||
SubscriptionStatusesQuery,
|
||||
TransactionHistoryQuery,
|
||||
TransactionInfoResponse,
|
||||
} from './Models'
|
||||
import { AppStoreError } from './Errors'
|
||||
|
||||
type HTTPMethod = 'GET' | 'POST'
|
||||
|
||||
interface QueryConvertible {
|
||||
[key: string]: string | number | boolean | number[]
|
||||
}
|
||||
|
||||
export class AppStoreServerAPI {
|
||||
// The maximum age that an authentication token is allowed to have, as decided by Apple.
|
||||
static readonly maxTokenAge: number = 3600 // seconds, = 1 hour
|
||||
|
||||
readonly environment: Environment
|
||||
private readonly baseUrl: string
|
||||
|
||||
private readonly key: Promise<jose.KeyLike>
|
||||
private readonly keyId: string
|
||||
private readonly issuerId: string
|
||||
private readonly bundleId: string
|
||||
private token?: string
|
||||
private tokenExpiry: Date = new Date(0)
|
||||
|
||||
/**
|
||||
* @param key the key downloaded from App Store Connect in PEM-encoded PKCS8 format.
|
||||
* @param keyId the id of the key, retrieved from App Store Connect
|
||||
* @param issuerId your issuer ID, retrieved from App Store Connect
|
||||
* @param bundleId bundle ID of your app
|
||||
*/
|
||||
constructor(key: string, keyId: string, issuerId: string, bundleId: string, environment = Environment.Production) {
|
||||
this.key = jose.importPKCS8(key, 'ES256')
|
||||
this.keyId = keyId
|
||||
this.issuerId = issuerId
|
||||
this.bundleId = bundleId
|
||||
this.environment = environment
|
||||
|
||||
if (environment === Environment.Sandbox) {
|
||||
this.baseUrl = 'https://api.storekit-sandbox.itunes.apple.com'
|
||||
} else {
|
||||
this.baseUrl = 'https://api.storekit.itunes.apple.com'
|
||||
}
|
||||
}
|
||||
|
||||
// API Endpoints
|
||||
|
||||
/**
|
||||
* https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history
|
||||
*/
|
||||
async getTransactionHistory(transactionId: string, query: TransactionHistoryQuery = {}): Promise<HistoryResponse> {
|
||||
const path = this.addQuery(`/inApps/v1/history/${transactionId}`, { ...query })
|
||||
return this.makeRequest('GET', path)
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info
|
||||
*/
|
||||
async getTransactionInfo(transactionId: string): Promise<TransactionInfoResponse> {
|
||||
return this.makeRequest('GET', `/inApps/v1/transactions/${transactionId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses
|
||||
*/
|
||||
async getSubscriptionStatuses(transactionId: string, query: SubscriptionStatusesQuery = {}): Promise<StatusResponse> {
|
||||
const path = this.addQuery(`/inApps/v1/subscriptions/${transactionId}`, { ...query })
|
||||
return this.makeRequest('GET', path)
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id
|
||||
*/
|
||||
async lookupOrder(orderId: string): Promise<OrderLookupResponse> {
|
||||
return this.makeRequest('GET', `/inApps/v1/lookup/${orderId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification
|
||||
*/
|
||||
async requestTestNotification(): Promise<SendTestNotificationResponse> {
|
||||
return this.makeRequest('POST', '/inApps/v1/notifications/test')
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.apple.com/documentation/appstoreserverapi/get_test_notification_status
|
||||
*/
|
||||
async getTestNotificationStatus(id: string): Promise<CheckTestNotificationResponse> {
|
||||
return this.makeRequest('GET', `/inApps/v1/notifications/test/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.apple.com/documentation/appstoreserverapi/get_notification_history
|
||||
*/
|
||||
async getNotificationHistory(
|
||||
request: NotificationHistoryRequest,
|
||||
query: NotificationHistoryQuery = {},
|
||||
): Promise<NotificationHistoryResponse> {
|
||||
const path = this.addQuery('/inApps/v1/notifications/history', { ...query })
|
||||
return this.makeRequest('POST', path, request)
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.apple.com/documentation/appstoreserverapi/send_consumption_information
|
||||
*/
|
||||
async sendConsumption(transactionId: string, request: ConsumptionRequest) {
|
||||
const path = `/inApps/v1/transactions/consumption/${transactionId}`
|
||||
return this.makeRequest('POST', path, request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a network request against the API and handles the result.
|
||||
*/
|
||||
private async makeRequest(method: HTTPMethod, path: string, body?: any): Promise<any> {
|
||||
const token = await this.getToken()
|
||||
const url = this.baseUrl + path
|
||||
const serializedBody = body ? JSON.stringify(body) : undefined
|
||||
|
||||
const result = await fetch(url, {
|
||||
method: method,
|
||||
body: serializedBody,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (result.status === 200) {
|
||||
return result.json()
|
||||
}
|
||||
|
||||
switch (result.status) {
|
||||
case 400:
|
||||
case 404:
|
||||
case 500:
|
||||
const body = await result.json()
|
||||
throw new AppStoreError(body.errorCode, body.errorMessage)
|
||||
|
||||
case 401:
|
||||
this.token = undefined
|
||||
throw new Error('The request is unauthorized; the JSON Web Token (JWT) is invalid.')
|
||||
|
||||
default:
|
||||
throw new Error('An unknown error occurred')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an existing authentication token (if its still valid) or generates a new one.
|
||||
*/
|
||||
private async getToken(): Promise<string> {
|
||||
// Reuse previously created token if it hasn't expired.
|
||||
if (this.token && !this.tokenExpired) return this.token
|
||||
|
||||
// Tokens must expire after at most 1 hour.
|
||||
const now = new Date()
|
||||
const expiry = new Date(now.getTime() + AppStoreServerAPI.maxTokenAge * 1000)
|
||||
const expirySeconds = Math.floor(expiry.getTime() / 1000)
|
||||
|
||||
const payload = {
|
||||
bid: this.bundleId,
|
||||
nonce: uuidv4(),
|
||||
}
|
||||
|
||||
const privateKey = await this.key
|
||||
|
||||
const jwt = await new jose.SignJWT(payload)
|
||||
.setProtectedHeader({ alg: 'ES256', kid: this.keyId, typ: 'JWT' })
|
||||
.setIssuer(this.issuerId)
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(expirySeconds)
|
||||
.setAudience('appstoreconnect-v1')
|
||||
.sign(privateKey)
|
||||
|
||||
this.token = jwt
|
||||
this.tokenExpiry = expiry
|
||||
|
||||
return jwt
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the previously generated token can still be used.
|
||||
*/
|
||||
private get tokenExpired(): boolean {
|
||||
// We consider the token to be expired slightly before it actually is to allow for some networking latency.
|
||||
const headroom = 60 // seconds
|
||||
const now = new Date()
|
||||
const cutoff = new Date(now.getTime() - headroom * 1000)
|
||||
|
||||
return !this.tokenExpiry || this.tokenExpiry < cutoff
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a query object into a query string and appends it
|
||||
* the provided path.
|
||||
*/
|
||||
private addQuery(path: string, query: QueryConvertible): string {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
params.append(key, item.toString())
|
||||
}
|
||||
} else {
|
||||
params.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = params.toString()
|
||||
|
||||
if (queryString === '') {
|
||||
return path
|
||||
} else {
|
||||
return `${path}?${queryString}`
|
||||
}
|
||||
}
|
||||
}
|
5
src/apple/AppleRootCertificate.ts
Normal file
5
src/apple/AppleRootCertificate.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// SHA-256 fingerprint of Apple's "Apple Root CA - G3 Root" certificate.
|
||||
// The root certificate can be obtained here: https://www.apple.com/certificateauthority/
|
||||
// This is used to verify that JWS have been signed using a key coming from Apple.
|
||||
export const APPLE_ROOT_CA_G3_FINGERPRINT =
|
||||
'63:34:3A:BF:B8:9A:6A:03:EB:B5:7E:9B:3F:5F:A7:BE:7C:4F:5C:75:6F:30:17:B3:A8:C4:88:C3:65:3E:91:79'
|
102
src/apple/Decoding.ts
Normal file
102
src/apple/Decoding.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { X509Certificate } from 'crypto'
|
||||
import * as jose from 'jose'
|
||||
import { CertificateValidationError } from './Errors'
|
||||
import {
|
||||
DecodedNotificationPayload,
|
||||
JWSRenewalInfo,
|
||||
JWSRenewalInfoDecodedPayload,
|
||||
JWSTransaction,
|
||||
JWSTransactionDecodedPayload,
|
||||
} from './Models'
|
||||
import { APPLE_ROOT_CA_G3_FINGERPRINT } from './AppleRootCertificate'
|
||||
|
||||
export async function decodeTransactions(
|
||||
signedTransactions: JWSTransaction[],
|
||||
rootCertFingerprint?: string,
|
||||
): Promise<JWSTransactionDecodedPayload[]> {
|
||||
return Promise.all(signedTransactions.map(transaction => decodeJWS(transaction, rootCertFingerprint)))
|
||||
}
|
||||
|
||||
export async function decodeTransaction(
|
||||
transaction: JWSTransaction,
|
||||
rootCertFingerprint?: string,
|
||||
): Promise<JWSTransactionDecodedPayload> {
|
||||
return decodeJWS(transaction, rootCertFingerprint)
|
||||
}
|
||||
|
||||
export async function decodeRenewalInfo(
|
||||
info: JWSRenewalInfo,
|
||||
rootCertFingerprint?: string,
|
||||
): Promise<JWSRenewalInfoDecodedPayload> {
|
||||
return decodeJWS(info, rootCertFingerprint)
|
||||
}
|
||||
|
||||
export async function decodeNotificationPayload(
|
||||
payload: string,
|
||||
rootCertFingerprint?: string,
|
||||
): Promise<DecodedNotificationPayload> {
|
||||
return decodeJWS(payload, rootCertFingerprint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes and verifies an object signed by the App Store according to JWS.
|
||||
* See: https://developer.apple.com/documentation/appstoreserverapi/jwstransaction
|
||||
* @param token JWS token
|
||||
* @param rootCertFingerprint Root certificate to validate against. Defaults to Apple's G3 CA but can be overriden for testing purposes.
|
||||
*/
|
||||
async function decodeJWS(token: string, rootCertFingerprint: string = APPLE_ROOT_CA_G3_FINGERPRINT): Promise<any> {
|
||||
// Extracts the key used to sign the JWS from the header of the token
|
||||
const getKey: jose.CompactVerifyGetKey = async (protectedHeader, _token) => {
|
||||
// RC 7515 stipulates that the key used to sign the JWS must be the first in the chain.
|
||||
// https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6
|
||||
|
||||
// jose will not import the certificate unless it is in a proper PKCS8 format.
|
||||
const certs = protectedHeader.x5c?.map(c => `-----BEGIN CERTIFICATE-----\n${c}\n-----END CERTIFICATE-----`) ?? []
|
||||
|
||||
validateCertificates(certs, rootCertFingerprint)
|
||||
|
||||
return jose.importX509(certs[0], 'ES256')
|
||||
}
|
||||
|
||||
const { payload } = await jose.compactVerify(token, getKey)
|
||||
|
||||
const decoded = new TextDecoder().decode(payload)
|
||||
const json = JSON.parse(decoded)
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a certificate chain provided in the x5c field of a decoded header of a JWS.
|
||||
* The certificates must be valid and have been signed by the provided
|
||||
* @param certificates A chain of certificates
|
||||
* @param rootCertFingerprint Expected SHA256 signature of the root certificate
|
||||
* @throws {CertificateValidationError} if any of the validation checks fail
|
||||
*/
|
||||
function validateCertificates(certificates: string[], rootCertFingerprint: string) {
|
||||
if (certificates.length === 0) throw new CertificateValidationError([])
|
||||
|
||||
const x509certs = certificates.map(c => new X509Certificate(c))
|
||||
|
||||
// Check dates
|
||||
const now = new Date()
|
||||
const datesValid = x509certs.every(c => new Date(c.validFrom) < now && now < new Date(c.validTo))
|
||||
if (!datesValid) throw new CertificateValidationError(certificates)
|
||||
|
||||
// Check that each certificate, except for the last, is issued by the subsequent one.
|
||||
if (certificates.length >= 2) {
|
||||
for (let i = 0; i < x509certs.length - 1; i++) {
|
||||
const subject = x509certs[i]
|
||||
const issuer = x509certs[i + 1]
|
||||
|
||||
if (subject.checkIssued(issuer) === false || subject.verify(issuer.publicKey) === false) {
|
||||
throw new CertificateValidationError(certificates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the last certificate in the chain is the expected root CA.
|
||||
if (x509certs[x509certs.length - 1].fingerprint256 !== rootCertFingerprint) {
|
||||
throw new CertificateValidationError(certificates)
|
||||
}
|
||||
}
|
33
src/apple/Errors.ts
Normal file
33
src/apple/Errors.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export class AppStoreError extends Error {
|
||||
// The following errors indicate that the request can be tried again.
|
||||
// See https://developer.apple.com/documentation/appstoreserverapi/error_codes
|
||||
// for a list of all errors.
|
||||
static readonly RETRYABLE_ERRORS = [
|
||||
4040002, // AccountNotFoundRetryableError
|
||||
4040004, // AppNotFoundRetryableError
|
||||
5000001, // GeneralInternalRetryableError
|
||||
4040006, // OriginalTransactionIdNotFoundRetryableError
|
||||
]
|
||||
|
||||
errorCode: number
|
||||
isRetryable: boolean
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/ratelimitexceedederror
|
||||
isRateLimitExceeded: boolean
|
||||
|
||||
constructor(errorCode: number, errorMessage: string) {
|
||||
super(errorMessage)
|
||||
this.errorCode = errorCode
|
||||
this.isRetryable = AppStoreError.RETRYABLE_ERRORS.includes(errorCode)
|
||||
this.isRateLimitExceeded = errorCode === 4290000
|
||||
}
|
||||
}
|
||||
|
||||
export class CertificateValidationError extends Error {
|
||||
certificates: string[]
|
||||
|
||||
constructor(certificates: string[]) {
|
||||
super('Certificate validation failed')
|
||||
this.certificates = certificates
|
||||
}
|
||||
}
|
535
src/apple/Models.ts
Normal file
535
src/apple/Models.ts
Normal file
@ -0,0 +1,535 @@
|
||||
export enum Environment {
|
||||
Production = 'Production',
|
||||
Sandbox = 'Sandbox',
|
||||
}
|
||||
|
||||
/**
|
||||
* UNIX timestamp in milliseconds
|
||||
*/
|
||||
export type Timestamp = number
|
||||
|
||||
/**
|
||||
* ISO 3166-1 Alpha-3 country code
|
||||
* https://developer.apple.com/documentation/appstoreservernotifications/storefrontcountrycode
|
||||
*/
|
||||
export type StorefrontCountryCode = string
|
||||
|
||||
export enum SortParameter {
|
||||
Ascending = 'ASCENDING',
|
||||
Descending = 'DESCENDING',
|
||||
}
|
||||
|
||||
export enum ProductTypeParameter {
|
||||
AutoRenewable = 'AUTO_RENEWABLE',
|
||||
NonRenewable = 'NON_RENEWABLE',
|
||||
Consumable = 'CONSUMABLE',
|
||||
NonConsumable = 'NON_CONSUMABLE',
|
||||
}
|
||||
|
||||
/**
|
||||
* The query parameters that can be passed to the history endpoint
|
||||
* to filter results and change sort order.
|
||||
* https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history
|
||||
*/
|
||||
export interface TransactionHistoryQuery {
|
||||
revision?: string
|
||||
sort?: SortParameter
|
||||
startDate?: Timestamp
|
||||
endDate?: Timestamp
|
||||
productType?: ProductTypeParameter
|
||||
productId?: string
|
||||
subscriptionGroupIdentifier?: string
|
||||
inAppOwnershipType?: OwnershipType
|
||||
revoked?: boolean
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/historyresponse
|
||||
export interface HistoryResponse {
|
||||
appAppleId: string
|
||||
bundleId: string
|
||||
environment: Environment
|
||||
hasMore: boolean
|
||||
revision: string
|
||||
signedTransactions: JWSTransaction[]
|
||||
}
|
||||
|
||||
export interface TransactionInfoResponse {
|
||||
signedTransactionInfo: JWSTransaction
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/jwstransaction
|
||||
export type JWSTransaction = string
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/jwsdecodedheader
|
||||
export interface JWSDecodedHeader {
|
||||
alg: string
|
||||
kid: string
|
||||
x5c: string[]
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload
|
||||
export interface JWSTransactionDecodedPayload {
|
||||
appAccountToken?: string
|
||||
bundleId: string
|
||||
environment: Environment
|
||||
expiresDate?: Timestamp
|
||||
inAppOwnershipType: OwnershipType
|
||||
isUpgraded?: boolean
|
||||
offerIdentifier?: string
|
||||
offerType?: OfferType
|
||||
originalPurchaseDate: Timestamp
|
||||
originalTransactionId: string
|
||||
productId: string
|
||||
purchaseDate: Timestamp
|
||||
quantity: number
|
||||
revocationDate?: Timestamp
|
||||
revocationReason?: number
|
||||
signedDate: Timestamp
|
||||
storefront: StorefrontCountryCode
|
||||
storefrontId: string
|
||||
subscriptionGroupIdentifier?: string
|
||||
transactionId: string
|
||||
transactionReason: TransactionReason
|
||||
type: TransactionType
|
||||
webOrderLineItemId: string
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype
|
||||
export enum OwnershipType {
|
||||
Purchased = 'PURCHASED',
|
||||
FamilyShared = 'FAMILY_SHARED',
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/type
|
||||
export enum TransactionType {
|
||||
AutoRenewableSubscription = 'Auto-Renewable Subscription',
|
||||
NonConsumable = 'Non-Consumable',
|
||||
Consumable = 'Consumable',
|
||||
NonRenewingSubscription = 'Non-Renewing Subscription',
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreservernotifications/transactionreason
|
||||
export enum TransactionReason {
|
||||
Purchase = 'PURCHASE',
|
||||
Renewal = 'RENEWAL',
|
||||
}
|
||||
|
||||
export interface SubscriptionStatusesQuery {
|
||||
status?: SubscriptionStatus[]
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/statusresponse
|
||||
export interface StatusResponse {
|
||||
data: SubscriptionGroupIdentifierItem[]
|
||||
environment: Environment
|
||||
appAppleId: string
|
||||
bundleId: string
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifieritem
|
||||
export interface SubscriptionGroupIdentifierItem {
|
||||
subscriptionGroupIdentifier: string
|
||||
lastTransactions: LastTransactionsItem[]
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/lasttransactionsitem
|
||||
export interface LastTransactionsItem {
|
||||
originalTransactionId: string
|
||||
status: SubscriptionStatus
|
||||
signedRenewalInfo: JWSRenewalInfo
|
||||
signedTransactionInfo: JWSTransaction
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo
|
||||
export type JWSRenewalInfo = string
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/status
|
||||
export enum SubscriptionStatus {
|
||||
Active = 1,
|
||||
Expired = 2,
|
||||
InBillingRetry = 3,
|
||||
InBillingGracePeriod = 4,
|
||||
Revoked = 5,
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfodecodedpayload
|
||||
export interface JWSRenewalInfoDecodedPayload {
|
||||
autoRenewProductId: string
|
||||
autoRenewStatus: AutoRenewStatus
|
||||
environment: Environment
|
||||
expirationIntent?: ExpirationIntent
|
||||
gracePeriodExpiresDate?: Timestamp
|
||||
isInBillingRetryPeriod?: boolean
|
||||
offerIdentifier?: string
|
||||
offerType?: OfferType
|
||||
originalTransactionId: string
|
||||
priceIncreaseStatus?: PriceIncreaseStatus
|
||||
productId: string
|
||||
recentSubscriptionStartDate: Timestamp
|
||||
renewalDate: Timestamp
|
||||
signedDate: Timestamp
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus
|
||||
export enum AutoRenewStatus {
|
||||
Off = 0,
|
||||
On = 1,
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/expirationintent
|
||||
export enum ExpirationIntent {
|
||||
Canceled = 1,
|
||||
BillingError = 2,
|
||||
RejectedPriceIncrease = 3,
|
||||
ProductUnavailable = 4,
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/offertype
|
||||
export enum OfferType {
|
||||
Introductory = 1,
|
||||
Promotional = 2,
|
||||
SubscriptionOfferCode = 3,
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus
|
||||
export enum PriceIncreaseStatus {
|
||||
NoResponse = 0,
|
||||
Consented = 1,
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/orderlookupresponse
|
||||
export interface OrderLookupResponse {
|
||||
status: OrderLookupStatus
|
||||
signedTransactions: JWSTransaction[]
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus
|
||||
export enum OrderLookupStatus {
|
||||
Valid = 0,
|
||||
Invalid = 1,
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload
|
||||
export interface DecodedNotificationPayload {
|
||||
notificationType: NotificationType
|
||||
subtype?: NotificationSubtype
|
||||
notificationUUID: string
|
||||
version: string
|
||||
signedDate: Timestamp
|
||||
data: NotificationData
|
||||
summary: NotificationSummary
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreservernotifications/data
|
||||
export interface NotificationData {
|
||||
appAppleId: string
|
||||
bundleId: string
|
||||
bundleVersion: number
|
||||
environment: Environment
|
||||
signedRenewalInfo: JWSRenewalInfo
|
||||
signedTransactionInfo: JWSTransaction
|
||||
status?: SubscriptionStatus
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreservernotifications/summary
|
||||
export interface NotificationSummary {
|
||||
requestIdentifier: string
|
||||
environment: Environment
|
||||
appAppleId: string
|
||||
bundleId: string
|
||||
productId: string
|
||||
storefrontCountryCodes?: StorefrontCountryCode[]
|
||||
failedCount: number
|
||||
succeededCount: number
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreservernotifications/notificationtype
|
||||
export enum NotificationType {
|
||||
ConsumptionRequest = 'CONSUMPTION_REQUEST',
|
||||
DidChangeRenewalPref = 'DID_CHANGE_RENEWAL_PREF',
|
||||
DidChangeRenewalStatus = 'DID_CHANGE_RENEWAL_STATUS',
|
||||
DidFailToRenew = 'DID_FAIL_TO_RENEW',
|
||||
DidRenew = 'DID_RENEW',
|
||||
Expired = 'EXPIRED',
|
||||
GracePeriodExpired = 'GRACE_PERIOD_EXPIRED',
|
||||
OfferRedeemed = 'OFFER_REDEEMED',
|
||||
PriceIncrease = 'PRICE_INCREASE',
|
||||
Refund = 'REFUND',
|
||||
RefundDeclined = 'REFUND_DECLINED',
|
||||
RenewalExtended = 'RENEWAL_EXTENDED',
|
||||
Revoke = 'REVOKE',
|
||||
Subscribed = 'SUBSCRIBED',
|
||||
RenewalExtension = 'RENEWAL_EXTENSION',
|
||||
RefundReversed = 'REFUND_REVERSED',
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreservernotifications/subtype
|
||||
export enum NotificationSubtype {
|
||||
InitialBuy = 'INITIAL_BUY',
|
||||
Resubscribe = 'RESUBSCRIBE',
|
||||
Downgrade = 'DOWNGRADE',
|
||||
Upgrade = 'UPGRADE',
|
||||
AutoRenewEnabled = 'AUTO_RENEW_ENABLED',
|
||||
AutoRenewDisabled = 'AUTO_RENEW_DISABLED',
|
||||
Voluntary = 'VOLUNTARY',
|
||||
BillingRetry = 'BILLING_RETRY',
|
||||
PriceIncrease = 'PRICE_INCREASE',
|
||||
GracePeriod = 'GRACE_PERIOD',
|
||||
BillingRecovery = 'BILLING_RECOVERY',
|
||||
Pending = 'PENDING',
|
||||
Accepted = 'ACCEPTED',
|
||||
Summary = 'SUMMARY',
|
||||
Failure = 'FAILURE',
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/sendtestnotificationresponse
|
||||
export interface SendTestNotificationResponse {
|
||||
testNotificationToken: string
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/checktestnotificationresponse
|
||||
export interface CheckTestNotificationResponse {
|
||||
sendAttempts: SendAttempt[]
|
||||
signedPayload: string
|
||||
}
|
||||
|
||||
export interface SendAttempt {
|
||||
attemptDate: Timestamp
|
||||
sendAttemptResult: SendAttemptResult
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult
|
||||
export enum SendAttemptResult {
|
||||
Success = 'SUCCESS',
|
||||
TimedOut = 'TIMED_OUT',
|
||||
TlsIssue = 'TLS_ISSUE',
|
||||
CircularRedirect = 'CIRCULAR_REDIRECT',
|
||||
NoResponse = 'NO_RESPONSE',
|
||||
SocketIssue = 'SOCKET_ISSUE',
|
||||
UnsupportedCharset = 'UNSUPPORTED_CHARSET',
|
||||
InvalidResponse = 'INVALID_RESPONSE',
|
||||
PrematureClose = 'PREMATURE_CLOSE',
|
||||
Other = 'OTHER',
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/get_notification_history
|
||||
export interface NotificationHistoryQuery {
|
||||
paginationToken?: string
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryrequest
|
||||
export interface NotificationHistoryRequest {
|
||||
startDate: Timestamp
|
||||
endDate: Timestamp
|
||||
notificationType?: NotificationType
|
||||
notificationSubtype?: NotificationSubtype
|
||||
onlyFailures?: boolean
|
||||
transactionId?: string
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponse
|
||||
export interface NotificationHistoryResponse {
|
||||
notificationHistory: NotificationHistoryResponseItem[]
|
||||
hasMore: boolean
|
||||
paginationToken: string
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem
|
||||
export interface NotificationHistoryResponseItem {
|
||||
sendAttempts: SendAttempt[]
|
||||
signedPayload: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The platform on which the customer consumed the in-app purchase.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/platform platform}
|
||||
*/
|
||||
export enum Platform {
|
||||
UNDECLARED = 0,
|
||||
APPLE = 1,
|
||||
NON_APPLE = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* A value that indicates the extent to which the customer consumed the in-app purchase.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus consumptionStatus}
|
||||
*/
|
||||
export enum ConsumptionStatus {
|
||||
UNDECLARED = 0,
|
||||
NOT_CONSUMED = 1,
|
||||
PARTIALLY_CONSUMED = 2,
|
||||
FULLY_CONSUMED = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* A value that indicates whether the app successfully delivered an in-app purchase that works properly.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/deliverystatus deliveryStatus}
|
||||
*/
|
||||
export enum DeliveryStatus {
|
||||
DELIVERED_AND_WORKING_PROPERLY = 0,
|
||||
DID_NOT_DELIVER_DUE_TO_QUALITY_ISSUE = 1,
|
||||
DELIVERED_WRONG_ITEM = 2,
|
||||
DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE = 3,
|
||||
DID_NOT_DELIVER_DUE_TO_IN_GAME_CURRENCY_CHANGE = 4,
|
||||
DID_NOT_DELIVER_FOR_OTHER_REASON = 5,
|
||||
}
|
||||
|
||||
/**
|
||||
* The age of the customer’s account.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/accounttenure accountTenure}
|
||||
*/
|
||||
export enum AccountTenure {
|
||||
UNDECLARED = 0,
|
||||
ZERO_TO_THREE_DAYS = 1,
|
||||
THREE_DAYS_TO_TEN_DAYS = 2,
|
||||
TEN_DAYS_TO_THIRTY_DAYS = 3,
|
||||
THIRTY_DAYS_TO_NINETY_DAYS = 4,
|
||||
NINETY_DAYS_TO_ONE_HUNDRED_EIGHTY_DAYS = 5,
|
||||
ONE_HUNDRED_EIGHTY_DAYS_TO_THREE_HUNDRED_SIXTY_FIVE_DAYS = 6,
|
||||
GREATER_THAN_THREE_HUNDRED_SIXTY_FIVE_DAYS = 7,
|
||||
}
|
||||
|
||||
/**
|
||||
* A value that indicates the amount of time that the customer used the app.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/playtime playTime}
|
||||
*/
|
||||
export enum PlayTime {
|
||||
UNDECLARED = 0,
|
||||
ZERO_TO_FIVE_MINUTES = 1,
|
||||
FIVE_TO_SIXTY_MINUTES = 2,
|
||||
ONE_TO_SIX_HOURS = 3,
|
||||
SIX_HOURS_TO_TWENTY_FOUR_HOURS = 4,
|
||||
ONE_DAY_TO_FOUR_DAYS = 5,
|
||||
FOUR_DAYS_TO_SIXTEEN_DAYS = 6,
|
||||
OVER_SIXTEEN_DAYS = 7,
|
||||
}
|
||||
|
||||
/**
|
||||
* A value that indicates the dollar amount of refunds the customer has received in your app, since purchasing the app, across all platforms.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded lifetimeDollarsRefunded}
|
||||
*/
|
||||
export enum LifetimeDollarsRefunded {
|
||||
UNDECLARED = 0,
|
||||
ZERO_DOLLARS = 1,
|
||||
ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2,
|
||||
FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3,
|
||||
ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4,
|
||||
FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5,
|
||||
ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6,
|
||||
TWO_THOUSAND_DOLLARS_OR_GREATER = 7,
|
||||
}
|
||||
|
||||
/**
|
||||
* A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased lifetimeDollarsPurchased}
|
||||
*/
|
||||
export enum LifetimeDollarsPurchased {
|
||||
UNDECLARED = 0,
|
||||
ZERO_DOLLARS = 1,
|
||||
ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2,
|
||||
FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3,
|
||||
ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4,
|
||||
FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5,
|
||||
ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6,
|
||||
TWO_THOUSAND_DOLLARS_OR_GREATER = 7,
|
||||
}
|
||||
|
||||
/**
|
||||
* The status of a customer’s account within your app.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/userstatus userStatus}
|
||||
*/
|
||||
export enum UserStatus {
|
||||
UNDECLARED = 0,
|
||||
ACTIVE = 1,
|
||||
SUSPENDED = 2,
|
||||
TERMINATED = 3,
|
||||
LIMITED_ACCESS = 4,
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest
|
||||
export interface ConsumptionRequest {
|
||||
/**
|
||||
* A Boolean value that indicates whether the customer consented to provide consumption data to the App Store.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/customerconsented customerConsented}
|
||||
**/
|
||||
customerConsented?: boolean
|
||||
|
||||
/**
|
||||
* A value that indicates the extent to which the customer consumed the in-app purchase.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus consumptionStatus}
|
||||
**/
|
||||
consumptionStatus?: ConsumptionStatus
|
||||
|
||||
/**
|
||||
* A value that indicates the platform on which the customer consumed the in-app purchase.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/platform platform}
|
||||
**/
|
||||
platform?: Platform
|
||||
|
||||
/**
|
||||
* A Boolean value that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided sampleContentProvided}
|
||||
**/
|
||||
sampleContentProvided?: boolean
|
||||
|
||||
/**
|
||||
* A value that indicates whether the app successfully delivered an in-app purchase that works properly.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/deliverystatus deliveryStatus}
|
||||
**/
|
||||
deliveryStatus?: DeliveryStatus
|
||||
|
||||
/**
|
||||
* The UUID that an app optionally generates to map a customer’s in-app purchase with its resulting App Store transaction.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken appAccountToken}
|
||||
**/
|
||||
appAccountToken?: string
|
||||
|
||||
/**
|
||||
* The age of the customer’s account.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/accounttenure accountTenure}
|
||||
**/
|
||||
accountTenure?: AccountTenure
|
||||
|
||||
/**
|
||||
* A value that indicates the amount of time that the customer used the app.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest ConsumptionRequest}
|
||||
**/
|
||||
playTime?: PlayTime
|
||||
|
||||
/**
|
||||
* A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded lifetimeDollarsRefunded}
|
||||
**/
|
||||
lifetimeDollarsRefunded?: LifetimeDollarsRefunded
|
||||
|
||||
/**
|
||||
* A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased lifetimeDollarsPurchased}
|
||||
**/
|
||||
lifetimeDollarsPurchased?: LifetimeDollarsPurchased
|
||||
|
||||
/**
|
||||
* The status of the customer’s account.
|
||||
*
|
||||
* {@link https://developer.apple.com/documentation/appstoreserverapi/userstatus userStatus}
|
||||
**/
|
||||
userStatus?: UserStatus
|
||||
}
|
38
src/controllers/applepay.controller.ts
Normal file
38
src/controllers/applepay.controller.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { decodeNotificationPayload } from 'apple/Decoding'
|
||||
import { DecodedNotificationPayload } from 'apple/Models'
|
||||
import BaseController, { ROLE_ANON } from 'common/base.controller'
|
||||
import { role, router } from 'decorators/router'
|
||||
import logger from 'logger/logger'
|
||||
import { IosPaySvr } from 'service/iospay.svr'
|
||||
|
||||
class ApplePayController extends BaseController {
|
||||
@role(ROLE_ANON)
|
||||
@router('post /pay/apple/verify')
|
||||
async verifyApplePay(req, res) {
|
||||
// const user = req.user
|
||||
const { receipt } = req.params
|
||||
let transaction = await new IosPaySvr().getTransactionInfo(receipt)
|
||||
logger.info('getTransactionInfo:: ', transaction)
|
||||
let results = await new IosPaySvr().getTransactionHistory(receipt)
|
||||
logger.info('getTransactionHistory:: length: ', results.length)
|
||||
for (let result of results) {
|
||||
logger.info(result)
|
||||
}
|
||||
await new IosPaySvr().queryNotificationHistory()
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* apple in-app purchase server-to-server notification
|
||||
* use notification v2
|
||||
* https://developer.apple.com/documentation/appstoreservernotifications/app_store_server_notifications_v2
|
||||
*/
|
||||
@role(ROLE_ANON)
|
||||
@router('post /pay/out/apple/notify')
|
||||
async notifyApplePay(req, res) {
|
||||
let { signedPayload } = req.params
|
||||
const decodedNotificationPayload: DecodedNotificationPayload = await decodeNotificationPayload(signedPayload)
|
||||
logger.info('apple notification:: ', JSON.stringify(decodedNotificationPayload))
|
||||
return res.send('ok')
|
||||
}
|
||||
}
|
16
src/controllers/purchase.controller.ts
Normal file
16
src/controllers/purchase.controller.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import BaseController, { ROLE_ANON } from 'common/base.controller'
|
||||
import { ZError } from 'common/ZError'
|
||||
import { role, router } from 'decorators/router'
|
||||
import logger from 'logger/logger'
|
||||
|
||||
class PurchaseController extends BaseController {
|
||||
/**
|
||||
* //TODO::供内部二次确认订单信息使用
|
||||
*/
|
||||
@role(ROLE_ANON)
|
||||
@router('get /inner/purchase/:id')
|
||||
async getPurchaseRecord(req, res) {
|
||||
let { id, sign } = req.params
|
||||
return {}
|
||||
}
|
||||
}
|
76
src/service/iospay.svr.ts
Normal file
76
src/service/iospay.svr.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { AppStoreServerAPI } from 'apple/AppStoreServerAPI'
|
||||
import { decodeTransaction, decodeTransactions } from 'apple/Decoding'
|
||||
import { Environment, OrderLookupStatus, ProductTypeParameter, SortParameter } from 'apple/Models'
|
||||
import { singleton } from 'decorators/singleton'
|
||||
import logger from 'logger/logger'
|
||||
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
const sandbox = env === 'development' ? true : false
|
||||
|
||||
const KEY = `-----BEGIN PRIVATE KEY-----
|
||||
${process.env.IOS_PRIVATE_KEY}
|
||||
-----END PRIVATE KEY-----`
|
||||
|
||||
const KEY_ID = process.env.IOS_KEY_ID
|
||||
const ISSUER_ID = process.env.IOS_ISSUER_ID
|
||||
const APP_BUNDLE_ID = process.env.IOS_APP_BUNDLE_ID
|
||||
const environment = sandbox ? Environment.Sandbox : Environment.Production
|
||||
|
||||
const api = new AppStoreServerAPI(KEY, KEY_ID, ISSUER_ID, APP_BUNDLE_ID, environment)
|
||||
|
||||
@singleton
|
||||
export class IosPaySvr {
|
||||
public async iosPayVerify(orderId: string) {
|
||||
logger.info('iosPayVerify: ', orderId)
|
||||
const response = await api.lookupOrder(orderId)
|
||||
|
||||
if (response.status === OrderLookupStatus.Valid) {
|
||||
const transactions = await decodeTransactions(response.signedTransactions)
|
||||
logger.info('transactions: ', transactions)
|
||||
}
|
||||
}
|
||||
|
||||
public async getTransactionHistory(originalTransactionId: string, rversion: string = '') {
|
||||
let reqData = {
|
||||
productType: ProductTypeParameter.Consumable,
|
||||
sort: SortParameter.Descending,
|
||||
}
|
||||
if (rversion) {
|
||||
reqData['revision'] = rversion
|
||||
}
|
||||
const response = await api.getTransactionHistory(originalTransactionId, reqData)
|
||||
let results: any = []
|
||||
// Decoding not only reveals the contents of the transactions but also verifies that they were signed by Apple.
|
||||
const transactions = await decodeTransactions(response.signedTransactions)
|
||||
for (let transaction of transactions) {
|
||||
// Do something with your transactions...
|
||||
results.push(transaction)
|
||||
}
|
||||
// The response contains at most 20 entries. You can check to see if there are more.
|
||||
if (response.hasMore) {
|
||||
let sub = await this.getTransactionHistory(originalTransactionId, response.revision)
|
||||
results = results.concat(sub)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
public async getTransactionInfo(originalTransactionId: string) {
|
||||
const response = await api.getTransactionInfo(originalTransactionId)
|
||||
const transaction = await decodeTransaction(response.signedTransactionInfo)
|
||||
return transaction
|
||||
}
|
||||
|
||||
public async queryNotificationHistory() {
|
||||
// Start and end date are required.
|
||||
// The earliest supported start date is June 6th (the start of WWDC 2022).
|
||||
const response = await api.getNotificationHistory({
|
||||
startDate: 1690175687060, // June 6th 2022
|
||||
endDate: new Date().getTime(),
|
||||
})
|
||||
logger.info('queryNotificationHistory response: ', response)
|
||||
// Check if there are more items.
|
||||
if (response.hasMore) {
|
||||
// Use history.paginationToken to fetch additional items.
|
||||
}
|
||||
}
|
||||
}
|
@ -1466,6 +1466,11 @@ jose@^2.0.6:
|
||||
dependencies:
|
||||
"@panva/asn1.js" "^1.0.0"
|
||||
|
||||
jose@^4.14.4:
|
||||
version "4.14.4"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.4.tgz#59e09204e2670c3164ee24cbfe7115c6f8bff9ca"
|
||||
integrity sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==
|
||||
|
||||
js-sdsl@^4.1.4:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.npmmirror.com/js-sdsl/-/js-sdsl-4.1.5.tgz#1ff1645e6b4d1b028cd3f862db88c9d887f26e2a"
|
||||
|
Loading…
x
Reference in New Issue
Block a user