Skip to content

Commit

Permalink
Merge pull request #132 from alexanderjordanbaker/ASSAv1.11
Browse files Browse the repository at this point in the history
Add support for App Store Server API v1.11 and App Store Server Notif…
  • Loading branch information
alexanderjordanbaker committed May 6, 2024
2 parents b8149ff + 64ba6f6 commit 81f38d9
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 1 deletion.
9 changes: 9 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,9 +671,18 @@ export enum APIError {
* An error that indicates the transaction identifier doesn’t represent a consumable in-app purchase.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/invalidtransactionnotconsumableerror InvalidTransactionNotConsumableError}
*
* @deprecated
*/
INVALID_TRANSACTION_NOT_CONSUMABLE = 4000043,

/**
* An error that indicates the transaction identifier represents an unsupported in-app purchase type.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/invalidtransactiontypenotsupportederror InvalidTransactionTypeNotSupportedError}
*/
INVALID_TRANSACTION_TYPE_NOT_SUPPORTED = 4000047,

/**
* An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state.
*
Expand Down
8 changes: 8 additions & 0 deletions models/ConsumptionRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LifetimeDollarsPurchased } from "./LifetimeDollarsPurchased"
import { LifetimeDollarsRefunded } from "./LifetimeDollarsRefunded"
import { Platform } from "./Platform"
import { PlayTime } from "./PlayTime"
import { RefundPreference } from "./RefundPreference"
import { UserStatus } from "./UserStatus"

/**
Expand Down Expand Up @@ -92,4 +93,11 @@ export interface ConsumptionRequest {
* {@link https://developer.apple.com/documentation/appstoreserverapi/userstatus userStatus}
**/
userStatus?: UserStatus | number

/**
* A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/refundpreference refundPreference}
**/
refundPreference?: RefundPreference | number
}
18 changes: 18 additions & 0 deletions models/ConsumptionRequestReason.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) 2024 Apple Inc. Licensed under MIT License.

import { StringValidator } from "./Validator";

/**
* The customer-provided reason for a refund request.
*
* {@link https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason consumptionRequestReason}
*/
export enum ConsumptionRequestReason {
UNINTENDED_PURCHASE = "UNINTENDED_PURCHASE",
FULFILLMENT_ISSUE = "FULFILLMENT_ISSUE",
UNSATISFIED_WITH_PURCHASE = "UNSATISFIED_WITH_PURCHASE",
LEGAL = "LEGAL",
OTHER = "OTHER",
}

export class ConsumptionRequestReasonValidator extends StringValidator {}
12 changes: 12 additions & 0 deletions models/Data.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) 2023 Apple Inc. Licensed under MIT License.

import { ConsumptionRequestReason, ConsumptionRequestReasonValidator } from "./ConsumptionRequestReason"
import { Environment, EnvironmentValidator } from "./Environment"
import { Status, StatusValidator } from "./Status"
import { Validator } from "./Validator"
Expand Down Expand Up @@ -59,12 +60,20 @@ export interface Data {
* {@link https://developer.apple.com/documentation/appstoreservernotifications/status status}
**/
status?: Status | number

/**
* The reason the customer requested the refund.
*
* {@link https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason consumptionRequestReason}
**/
consumptionRequestReason?: ConsumptionRequestReason | string
}


export class DataValidator implements Validator<Data> {
static readonly environmentValidator = new EnvironmentValidator()
static readonly statusValidator = new StatusValidator()
static readonly consumptionRequestReasonValidator = new ConsumptionRequestReasonValidator()
validate(obj: any): obj is Data {
if ((typeof obj['environment'] !== 'undefined') && !(DataValidator.environmentValidator.validate(obj['environment']))) {
return false
Expand All @@ -87,6 +96,9 @@ export class DataValidator implements Validator<Data> {
if ((typeof obj['status'] !== 'undefined') && !(DataValidator.statusValidator.validate(obj['status']))) {
return false
}
if ((typeof obj['consumptionRequestReason'] !== 'undefined') && !(DataValidator.consumptionRequestReasonValidator.validate(obj['consumptionRequestReason']))) {
return false
}
return true
}
}
13 changes: 13 additions & 0 deletions models/RefundPreference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2024 Apple Inc. Licensed under MIT License.

/**
* A value that indicates your preferred outcome for the refund request.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/refundpreference refundPreference}
*/
export enum RefundPreference {
UNDECLARED = 0,
PREFER_GRANT = 1,
PREFER_DECLINE = 2,
NO_PREFERENCE = 3,
}
16 changes: 16 additions & 0 deletions tests/resources/models/signedConsumptionRequestNotification.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"notificationType": "CONSUMPTION_REQUEST",
"notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20",
"data": {
"environment": "LocalTesting",
"appAppleId": 41234,
"bundleId": "com.example",
"bundleVersion": "1.2.3",
"signedTransactionInfo": "signed_transaction_info_value",
"signedRenewalInfo": "signed_renewal_info_value",
"status": 1,
"consumptionRequestReason": "UNINTENDED_PURCHASE"
},
"version": "2.0",
"signedDate": 1698148900000
}
5 changes: 4 additions & 1 deletion tests/unit-tests/api_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Subtype } from "../../models/Subtype";
import { UserStatus } from "../../models/UserStatus";
import { readFile } from "../util"
import { InAppOwnershipType } from "../../models/InAppOwnershipType";
import { RefundPreference } from "../../models/RefundPreference";
import { APIError, APIException, AppStoreServerAPIClient, ExtendReasonCode, ExtendRenewalDateRequest, MassExtendRenewalDateRequest, NotificationHistoryRequest, NotificationHistoryResponseItem, Order, OrderLookupStatus, ProductType, SendAttemptResult, TransactionHistoryRequest } from "../../index";
import { Response } from "node-fetch";

Expand Down Expand Up @@ -387,6 +388,7 @@ describe('The api client ', () => {
expect(6).toBe(body.lifetimeDollarsRefunded)
expect(7).toBe(body.lifetimeDollarsPurchased)
expect(4).toBe(body.userStatus)
expect(3).toBe(body.refundPreference)
});

const consumptionRequest: ConsumptionRequest = {
Expand All @@ -400,7 +402,8 @@ describe('The api client ', () => {
playTime: PlayTime.ONE_DAY_TO_FOUR_DAYS,
lifetimeDollarsRefunded: LifetimeDollarsRefunded.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS,
lifetimeDollarsPurchased: LifetimeDollarsPurchased.TWO_THOUSAND_DOLLARS_OR_GREATER,
userStatus: UserStatus.LIMITED_ACCESS
userStatus: UserStatus.LIMITED_ACCESS,
refundPreference: RefundPreference.NO_PREFERENCE
}

client.sendConsumptionData("49571273", consumptionRequest);
Expand Down
24 changes: 24 additions & 0 deletions tests/unit-tests/transaction_decoding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { InAppOwnershipType } from "../../models/InAppOwnershipType";
import { RevocationReason } from "../../models/RevocationReason";
import { TransactionReason } from "../../models/TransactionReason";
import { Type } from "../../models/Type";
import { ConsumptionRequestReason } from "../../models/ConsumptionRequestReason";


describe('Testing decoding of signed data', () => {
Expand Down Expand Up @@ -102,6 +103,29 @@ describe('Testing decoding of signed data', () => {
expect("signed_transaction_info_value").toBe(notification.data!.signedTransactionInfo)
expect("signed_renewal_info_value").toBe(notification.data!.signedRenewalInfo)
expect(Status.ACTIVE).toBe(notification.data!.status)
expect(notification.data!.consumptionRequestReason).toBeFalsy()
})
it('should decode a signed CONSUMPTION_REQUEST notification', async () => {
const signedNotification = createSignedDataFromJson("tests/resources/models/signedConsumptionRequestNotification.json")

const notification = await getDefaultSignedPayloadVerifier().verifyAndDecodeNotification(signedNotification)

expect(NotificationTypeV2.CONSUMPTION_REQUEST).toBe(notification.notificationType)
expect(notification.subtype).toBeFalsy()
expect("002e14d5-51f5-4503-b5a8-c3a1af68eb20").toBe(notification.notificationUUID)
expect("2.0").toBe(notification.version)
expect(1698148900000).toBe(notification.signedDate)
expect(notification.data).toBeTruthy()
expect(notification.summary).toBeFalsy()
expect(notification.externalPurchaseToken).toBeFalsy()
expect(Environment.LOCAL_TESTING).toBe(notification.data!.environment)
expect(41234).toBe(notification.data!.appAppleId)
expect("com.example").toBe(notification.data!.bundleId)
expect("1.2.3").toBe(notification.data!.bundleVersion)
expect("signed_transaction_info_value").toBe(notification.data!.signedTransactionInfo)
expect("signed_renewal_info_value").toBe(notification.data!.signedRenewalInfo)
expect(Status.ACTIVE).toBe(notification.data!.status)
expect(ConsumptionRequestReason.UNINTENDED_PURCHASE).toBe(notification.data!.consumptionRequestReason)
})
it('should decode a signed summary notification', async () => {
const signedNotification = createSignedDataFromJson("tests/resources/models/signedSummaryNotification.json")
Expand Down

0 comments on commit 81f38d9

Please sign in to comment.