Giter Site home page Giter Site logo

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

Apple App Store Server Java Library

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

Table of Contents

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

Installation

Requirements

  • Java 11+

Gradle

implementation 'com.apple.itunes.storekit:app-store-server-library:2.2.0'

Maven

<dependency>
    <groupId>com.apple.itunes.storekit</groupId>
    <artifactId>app-store-server-library</artifactId>
    <version>2.2.0</version>
</dependency>

Documentation

JavaDocs

WWDC Video

Obtaining an In-App Purchase key from App Store Connect

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

Obtaining Apple Root Certificates

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

Usage

API Usage

import com.apple.itunes.storekit.client.APIException;
import com.apple.itunes.storekit.client.AppStoreServerAPIClient;
import com.apple.itunes.storekit.model.Environment;
import com.apple.itunes.storekit.model.SendTestNotificationResponse;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class APIExample {
    public static void main(String[] args) throws Exception {
        String issuerId = "99b16628-15e4-4668-972b-eeff55eeff55";
        String keyId = "ABCDEFGHIJ";
        String bundleId = "com.example";
        Path filePath = Path.of("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8");
        String encodedKey = Files.readString(filePath);
        Environment environment = Environment.SANDBOX;

        AppStoreServerAPIClient client =
                new AppStoreServerAPIClient(encodedKey, keyId, issuerId, bundleId, environment);

        try {
            SendTestNotificationResponse response = client.requestTestNotification();
            System.out.println(response);
        } catch (APIException | IOException e) {
            e.printStackTrace();
        }
    }
}

Verification Usage

import com.apple.itunes.storekit.model.Environment;
import com.apple.itunes.storekit.model.ResponseBodyV2DecodedPayload;
import com.apple.itunes.storekit.verification.SignedDataVerifier;
import com.apple.itunes.storekit.verification.VerificationException;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Set;

public class ExampleVerification {
    public static void main(String[] args) {
        String bundleId = "com.example";
        Environment environment = Environment.SANDBOX;
        Set<InputStream> rootCAs = Set.of(
                new FileInputStream("/path/to/rootCA1"),
                new FileInputStream("/path/to/rootCA2")
        );
        Long appAppleId = null; // appAppleId must be provided for the Production environment

        SignedDataVerifier signedPayloadVerifier = new SignedDataVerifier(rootCAs, bundleId, appAppleId, environment, true);
        
        String notificationPayload = "ey...";

        try {
            ResponseBodyV2DecodedPayload payload = signedPayloadVerifier.verifyAndDecodeNotification(notificationPayload);
            System.out.println(payload);
        } catch (VerificationException e) {
            e.printStackTrace();
        }
    }
}

Receipt Usage

import com.apple.itunes.storekit.client.AppStoreServerAPIClient;
import com.apple.itunes.storekit.client.GetTransactionHistoryVersion;
import com.apple.itunes.storekit.migration.ReceiptUtility;
import com.apple.itunes.storekit.model.Environment;
import com.apple.itunes.storekit.model.HistoryResponse;
import com.apple.itunes.storekit.model.TransactionHistoryRequest;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.List;

public class ExampleMigration {
    public static void main(String[] args) throws Exception {
        String issuerId = "99b16628-15e4-4668-972b-eeff55eeff55";
        String keyId = "ABCDEFGHIJ";
        String bundleId = "com.example";
        Path filePath = Path.of("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8");
        String encodedKey = Files.readString(filePath);
        Environment environment = Environment.SANDBOX;

        AppStoreServerAPIClient client =
                new AppStoreServerAPIClient(encodedKey, keyId, issuerId, bundleId, environment);

        String appReceipt = "MI...";
        ReceiptUtility receiptUtil = new ReceiptUtility();
        String transactionId = receiptUtil.extractTransactionIdFromAppReceipt(appReceipt);
        if (transactionId != null) {
            TransactionHistoryRequest request = new TransactionHistoryRequest()
                    .sort(TransactionHistoryRequest.Order.ASCENDING)
                    .revoked(false)
                    .productTypes(List.of(TransactionHistoryRequest.ProductType.AUTO_RENEWABLE));
            HistoryResponse response = null;
            List<String> transactions = new LinkedList<>();
            do {
                String revision = response != null ? response.getRevision() : null;
                response = client.getTransactionHistory(transactionId, revision, request, GetTransactionHistoryVersion.V2);
                transactions.addAll(response.getSignedTransactions());
            } while (response.getHasMore());
            System.out.println(transactions);
        }
    }
}

Promotional Offer Signature Creation

import com.apple.itunes.storekit.offers.PromotionalOfferSignatureCreator;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;

public class ExampleSignatureCreation {
    public static void main(String[] args) throws Exception {
        String keyId = "ABCDEFGHIJ";
        String bundleId = "com.example";
        Path filePath = Path.of("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8");
        String encodedKey = Files.readString(filePath);

        PromotionalOfferSignatureCreator signatureCreator = new PromotionalOfferSignatureCreator(encodedKey, keyId, bundleId);
        
        String productId = "<product_id>";
        String subscriptionOfferId = "<subscription_offer_id>";
        String applicationUsername = "<application_username>";
        UUID nonce = UUID.randomUUID();
        long timestamp = System.currentTimeMillis();
        String encodedSignature = signatureCreator.createSignature(productId, subscriptionOfferId, applicationUsername, nonce, timestamp);
        System.out.println(encodedSignature);
    }
}

Support

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

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

Contributors

alexanderjordanbaker avatar dependabot[bot] avatar hakusai22 avatar izanger avatar stshekh avatar vpavic avatar

Stargazers

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

Watchers

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

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

ReceiptUtility extracting transactionId fails

Hello,
I am currently working on a migration from verifyReceipt to the new API. When i try to extract transactionId from the receipt
using either receiptUtility.extractTransactionIdFromAppReceipt()/receiptUtility.extractTransactionIdFromTransactionReceipt(receipt)
I end up with
java.lang.IllegalArgumentException: Invalid App Receipt
or
java.lang.IllegalArgumentException: Invalid purchase-info
I've tested it on sandbox and prod receipts.

Thanks.

<dependency> <groupId>com.apple.itunes.storekit</groupId> <artifactId>app-store-server-library</artifactId> <version>1.0.0</version> </dependency>

`var receipt = "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";

var transactionId = receiptUtility.extractTransactionIdFromAppReceipt(receipt);`

Add proxy support

I have to setup a proxy to send,but it dosen't working

I had to modify the code of com.apple.itunes.storekit.client.AppStoreServerAPIClient.Code like this:

Proxy proxyTest = new Proxy(Proxy.Type.HTTP,new InetSocketAddress("proxy", proxyPort));
OkHttpClient client = new OkHttpClient.Builder().proxy(proxyTest).build();
AppStoreServerAPIClient client =
                new AppStoreServerAPIClient(encodedKey, keyId, issuerId, bundleId, environment,client);
public AppStoreServerAPIClient(String signingKey, String keyId, String issuerId, String bundleId, Environment environment,OkHttpClient httpClient) {
        this.bearerTokenAuthenticator = new BearerTokenAuthenticator(signingKey, keyId, issuerId, bundleId);
        this.httpClient = httpClient;
        this.urlBase = HttpUrl.parse(environment.equals(Environment.SANDBOX) ? SANDBOX_URL : PRODUCTION_URL);
        this.objectMapper = new ObjectMapper();
        objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker()
                .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
                .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
                .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)
                .withSetterVisibility(JsonAutoDetect.Visibility.NONE)
                .withCreatorVisibility(JsonAutoDetect.Visibility.NONE));
    }

PLEASE REPAIR IT.

ReceiptUtility.extractTransactionIdFromAppReceipt signature question

What's the criteria right now the transactionId is extracted from the appReceipt? Should not it return a Set of (original)transactionIds / a lit of all transactionIds?

Sometimes appReceipts may contain a lot of transactions that may have different originalTransactionIds (i.e. a regular subscription + ppvs (some of them may be active)).

Apple Root Certificates Update Schedule

Hello,
Should we download apple root certificates every deployment or periodically? (AppleComputerRootCertificate.cer expires on Feb 10 00:18:14 2025 GMT)
I could not find a direct answer on the web. I am not familiar with PKI and root certificates.
Thanks

【Enhance】Not found is_trial_period from JWSTransactionDecodedPayload

Purpose

I want to distinguish between trial subscriptions and regular subscriptions

First

the url , https://sandbox.itunes.apple.com/verifyReceipt , is deprecated.

Generally, the is_trial_period field corresponding to transaction_id is obtained according to the return result of the interface to distinguish whether to trial

but the url is deprecated , I also want to use the latest api

But

I use this SDK , such as:

            TransactionInfoResponse transactionInfo = appleClient.getTransactionInfo(transactionId);
            JWSTransactionDecodedPayload payload = appleVerifier.verifyAndDecodeTransaction(transactionInfo.getSignedTransactionInfo());

payload is :

{
    "originalTransactionId": "xxx",
    "transactionId": "xxx",
    "webOrderLineItemId": "xxx",
    "bundleId": "com.xxx.xxx",
    "productId": "com.xxx.xxx.xxx.0004",
    "subscriptionGroupIdentifier": "xxx",
    "purchaseDate": 1697509633000,
    "originalPurchaseDate": 1695127047000,
    "expiresDate": 1697509813000,
    "quantity": 1,
    "type": "Auto-Renewable Subscription",
    "inAppOwnershipType": "PURCHASED",
    "signedDate": 1697526007839,
    "offerType": "1",
    "environment": "Sandbox",
    "storefront": "CHN",
    "storefrontId": "xxx",
    "transactionReason": "PURCHASE"
}

I not found field of is_trial_period or able to identify trial fields

ConsumptionRequest model does not match the documentation

The documentation for the Send Consumption Information endpoint says the following:

The ConsumptionRequest response body requires that you set the appAccountToken value to a valid value of either a UUID or an empty string.

However, in the implementation of this library the appAccountToken is defined as an UUID object which cannot be serialized to an emptry string as it is not a valid UUID. This makes it impossible to use the model and send a ConsumptionRequest if we do not have an appAccountToken which is a valid case.

Add Default environment as Production for AppStoreServerAPIClient.java, for handling production usecase easily

Generally, while integrating the app to the production use case, its better to by default use the production api call and switch the endpoint in case the response code is 4040010. The decoded transaction info will already contain the correct environment, and it will be easier for the end users to integrate the library into their existing code base.

This makes the job for the end backend developer easier, as it will not require them to manually switch the context and they can do the separate processing if required for sandbox account based on the response from the client.

What should I do if I don’t know which app the callback information belongs to?

I have more than a dozen apps setting the same callback address in the background. My new AppStoreServerAPIClient(encodedKey, keyId, issuerId, bundleId, environment); I don’t know which app the callback information belongs to, so I don’t know who to change. Configuration information, is there a solution to this? For example, I don't verify the first layer of base64 and directly decrypt it. After getting the bundleId, can I determine which app it is?

image

Error base64 decode under windows system.

System: Windows 11

Jdk Version: 11

Bug Description: Decode certification throw Illegal base64 character a.

Reason: Most of Private Key files were generated under unix, the JDK method System.lineSeparator will return "\r\n" under windows rather "\n" under unix, cause cannot correctly replace the line separator in windows system.

Solution

signingKey = signingKey.replace("-----BEGIN PRIVATE KEY-----", "")
        // .replaceAll(System.lineSeparator(), "")  // error under windows system
        .replaceAll("\n", "")  // new 
        .replaceAll("\r", "")  // new
        .replace("-----END PRIVATE KEY-----", "");

Jackson cannot deserialize error payload when HTTP status code is 401

I was running locally in JDK21 with environment Xcode / LocalTesting. When I try to getTransactionHistory, if there is abnormal HTTP status code, jackson cannot deserialize it to ErrorPayload.class. 404 can deserialize, but 401 cannot. It should be due to HTTP 401 has an empty response body.

com.apple.itunes.storekit.client.APIException: Failed to call API with httpStatusCode=401
	at com.apple.itunes.storekit.client.AppStoreServerAPIClient.makeHttpCall(AppStoreServerAPIClient.java:132) ~[app-store-server-library-1.1.0.jar:na]
	at com.apple.itunes.storekit.client.AppStoreServerAPIClient.getTransactionHistory(AppStoreServerAPIClient.java:287) ~[app-store-server-library-1.1.0.jar:na]
.
.
.
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
 at [Source: (okhttp3.ResponseBody$BomAwareReader); line: 1, column: 0]
	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59) ~[jackson-databind-2.15.3.jar:2.15.3]
	at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4916) ~[jackson-databind-2.15.3.jar:2.15.3]
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4818) ~[jackson-databind-2.15.3.jar:2.15.3]
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3785) ~[jackson-databind-2.15.3.jar:2.15.3]
	at com.apple.itunes.storekit.client.AppStoreServerAPIClient.makeHttpCall(AppStoreServerAPIClient.java:126) ~[app-store-server-library-1.1.0.jar:na]

c.a.i.s.client.AppStoreServerAPIClient#extendSubscriptionRenewalDate throws 500 on no extend reason code and request identifier

I am writing logic to extend existing subscriptions for a certain amount of days. I am using the library for this. When not entering the extend reason code

val extendByDays = ExtendRenewalDateRequest().extendByDays(duration.inWholeDays.toInt())
val response = appStoreServerApiClientForProduction.extendSubscriptionRenewalDate(
            originalTransactionId.value,
            extendByDays,
)

I am noticing that when I try to execute this code, the library throws a HTTP 500 Internal Server Error where I would expect a HTTP 400 Bad Request.

If there is a extendReasonCode given, but no requestIdentifier, a HTTP 400 Bad Request is returned.

If there is no extendReasonCode given, but there is requestIdentifier given, a HTTP 500 Internal Server Error is thrown.

I suppose the latter is wrong and is a bug.

Improve test coverage

As evident by some recent issues like #32 and #37, test coverage in this project is quite low.

For example, SignedDataVerifier only verifies test notification type and not others that are truly relevant and ReceiptUtility is completely untested.

Not able to resolve imports after adding the dependency

Even after adding the dependencies it says Unresolved reference: apple

I am not able to resolve any of the imports

Here is what i did,
Added
implementation("com.apple.itunes.storekit:app-store-server-library:1.0.0")
to build.gradle

and tried importing, AppStoreServerAPIClient

When calling the api interface, there is a mismatch in the transmitted fields in the source code.

java.lang.NoSuchMethodError: 'com.auth0.jwt.JWTCreator$Builder com.auth0.jwt.JWTCreator$Builder.withExpiresAt(java.time.Instant)'

at com.apple.itunes.storekit.client.BearerTokenAuthenticator.generateToken(BearerTokenAuthenticator.java:51)
at com.apple.itunes.storekit.client.AppStoreServerAPIClient.makeRequest(AppStoreServerAPIClient.java:80)
at com.apple.itunes.storekit.client.AppStoreServerAPIClient.makeHttpCall(AppStoreServerAPIClient.java:106)
at com.apple.itunes.storekit.client.AppStoreServerAPIClient.getAllSubscriptionStatuses(AppStoreServerAPIClient.java:182)
at cn.lollypop.www.lollypopv2mallserver.client.appstorepay.AppStorePayClientTest.getAllSubscriptionStatuses(AppStorePayClientTest.java:52)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
            <dependency>
                <groupId>com.apple.itunes.storekit</groupId>
                <artifactId>app-store-server-library</artifactId>
                <version>1.0.0</version>
            </dependency>
image

Verification failed with status INVALID_CHAIN

{"response":{"body":"{\"errorCode\":10001,\"errorMsg\":\"com.apple.itunes.storekit.verification.VerificationException: Verification failed with status INVALID_CHAIN\"}","status":500},

hello Today, May 23rd 2024, 10:02:30.861 to May 23rd 2024, 13:51:27.308 (time zone is UTC+8) Our subscription callbacks encountered a large number of Verification failed with status INVALID_CHAIN. Not all callbacks are It failed. I want to know if Apple development has made any changes? And I didn’t see the specific reason from the source code library. The impact is to directly catch the INVALID_CHAIN exception.

image image

Error reported when using x5c certificate to decrypt signature

throwable:java.security.cert.CertPathValidatorException: Unable to determine revocation status due to network error
at sun.security.provider.certpath.PKIXMasterCertPathValidator.validate(PKIXMasterCertPathValidator.java:135)
at sun.security.provider.certpath.PKIXCertPathValidator.validate(PKIXCertPathValidator.java:233)
at sun.security.provider.certpath.PKIXCertPathValidator.validate(PKIXCertPathValidator.java:141)
at sun.security.provider.certpath.PKIXCertPathValidator.engineValidate(PKIXCertPathValidator.java:80)
at java.security.cert.CertPathValidator.validate(CertPathValidator.java:292)

How to call App Store Server API sandbox environment

I call transaction information for several apps. And I don't have environment information.

That's why I'm calling the sandbox environment as an official guide. However, unlike the official guide, certain apps sometimes use production URLs to call endpoints to receive 401 status codes and successfully call endpoints using the sandbox environment.

Why does this difference occur unlike the official guide?
Is there any way to determine if I should call into a sandbox environment when I receive a 401 status code from a production endpoint?

Sometimes it's a production transaction and JWT hasn't expired, but I'm getting a 401 status code response and calling it into a sandbox environment.

Please let me know the solution.

P8 certificate base64 decode exception

  • preview
    I create a client (AppStoreServerAPIClient client) as in the example,but I encounter an exception
Exception in thread "main" java.lang.IllegalArgumentException: Illegal base64 character a
	at java.base/java.util.Base64$Decoder.decode0(Base64.java:743)
	at java.base/java.util.Base64$Decoder.decode(Base64.java:535)
	at java.base/java.util.Base64$Decoder.decode(Base64.java:558)
	at com.apple.itunes.storekit.client.BearerTokenAuthenticator.<init>(BearerTokenAuthenticator.java:33)
	at com.apple.itunes.storekit.client.AppStoreServerAPIClient.<init>(AppStoreServerAPIClient.java:61)
  • debug
    I debug process , the apple sdk create server client , found the p8 certificate existing \n . that whill base64 decode fail

1695259586259

  • resolve
        # error example 
        // String encodedKey = Files.readString(filePath);
        # success example
        String encodedKey = Files.readString(filePath).replace("\n","");
  • suggestion

this should be done by the SDK

Decoded notification data does not contain status

Firstly, thanks for providing this library. Though still in beta phase, it has been very helpful to us while integrating with the App Store.

App Store server notifications documentation states that decoded payload notification data contains status of an auto-renewable subscription. However the class that maps this information, com.apple.itunes.storekit.model.Data, does not support this. I assume this is because the addition of status is quite recent, judging by server notifications changelog entry from June.

As subscription state is quite fundamental to managing subscriptions on the backend, could you please add support for this, a hopefully publish a 0.1.3 soon? I'm also willing to provide a PR if that would help speed up things a bit.

Invalid Environment enum

I occur error on decode NotificationV2 from app store
Cannot deserialize value of type com.apple.itunes.storekit.model.Environment from String "Sandbox": not one of the values accepted for Enum class: [PRODUCTION, SANDBOX]

Why not add field of price to JWSTransactionDecodedPayload class

https://developer.apple.com/documentation/appstoreservernotifications/jwstransactiondecodedpayload

In apple office docs , apple api will return price and currency

And I use JWT tool to decode signedTransactionInfo , it can decode price and currency , such as :

图片

The field of price and currency is necessary params , Why not add field of price to JWSTransactionDecodedPayload class ?

an exception appears with httpStatusCode=401, apiError=null, apiErrorMessage='null'

When calling the getAllSubscriptionStatuses(String transactionId, Status[] status) method in a production environment, an exception occasionally occurs, httpStatusCode=401, apiError=null, apiErrorMessage='null'. All my configurations are unified. When I create the AppStoreServerAPIClient, this exception will occasionally occur. I don't understand the triggering scenario of this accidental exception.

https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses
image

image
2024-05-04,02:05:40,233 ERROR a73a88cd [TID: N/A] cn.lollypop.www.xxxx.utils.RpcExceptionHandler: fail
cn.lollypop.common.base.exception.LollypopServerException: APIException{httpStatusCode=401, apiError=null, apiErrorMessage='null'}

Add method parameters in AppStoreServerAPIClient methods to accept bearer token

As a developer i think that while following a good design pattern, most of the services should create a single instance of the client for the production environment, providing a method to pass a bearer token, takes away the need to maintaining and refreshing the client every 5 minutes(expiry of the jwt bearer token). This still maintains security since the dev does not hardcode any secrets in config/code and still provides the flexibility to not create a client on the fly or maintain the overhead of refresh.

@alexanderjordanbaker what do you think, is it fine to open a pr for the same.

TRANSACTION_ID_NOT_FOUND with valid Transaction ID

I'm getting error TRANSACTION_ID_NOT_FOUND

I use transactionId from data from Apple. Example

{"receipt": {"productId": "threeDaysForGirl", "transactionDate": 1698142245000, "transactionId": "2000000442477236", "transactionReceipt":"***"}

I this case use transactionId "2000000442477236" in sendConsumptionData() or getTransactionInfo() methods invoke and getting 404 error.

APIException{httpStatusCode=404, apiError=TRANSACTION_ID_NOT_FOUND} com.apple.itunes.storekit.client.APIException
	at com.apple.itunes.storekit.client.AppStoreServerAPIClient.makeHttpCall(AppStoreServerAPIClient.java:114)
	at com.apple.itunes.storekit.client.AppStoreServerAPIClient.getTransactionInfo(AppStoreServerAPIClient.java:290)
	at ru.ruamo.web.impl.AppleServiceImpl.sendConsumptionRequest(AppleServiceImpl.java:65)
	at ru.ruamo.web.controllers.TestController.apl(TestController.java:397)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
	at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365)
	at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:100)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126)
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:131)
	at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:85)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:110)
	at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:101)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:151)
	at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:129)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
	at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
	at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.session.ForceEagerSessionCreationFilter.doFilterInternal(ForceEagerSessionCreationFilter.java:45)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
	at org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter.doFilterInternal(OpenEntityManagerInViewFilter.java:186)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:91)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
	at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:673)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:833)

Proxy configuration

Hi, I have to configure this library to use a proxy that requires authentication.

I see theres a previous issue opened about this #70, however the solution presented there don't quite work with auth.
From square/okhttp#4108, the okhttp library used doesn't respect java.net.Authenticator.setDefault, only java.net.ProxySelector.setDefault.

We have to set the authenticator in the client builder for it to be used.
https://square.github.io/okhttp/3.x/okhttp/okhttp3/OkHttpClient.Builder.html#proxyAuthenticator-okhttp3.Authenticator-

Are we able to add a constructor for the AppStoreServerAPIClient that lets us pass in the OkHttpClient?

Serialization errors with transaction model

com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type com.apple.itunes.storekit.model.Type from String "Auto-Renewable Subscription": not one of the values accepted for Enum class: [CONSUMABLE, AUTO_RENEWABLE_SUBSCRIPTION, NON_CONSUMABLE, NON_RENEWING_SUBSCRIPTION]

Consider renaming `Status` enums to avoid clashes

The project contains 2 enums named Status:

  • com.apple.itunes.storekit.model.Status
  • com.apple.itunes.storekit.verification.Status

This is unfortunate because if one needs to use both in the same class, one has to be referred to with fully-qualified class name.

Perhaps these enums could be renamed to be more specific (for example, SubscriptionStatus and VerificationStatus)?

Requirements from app reviewer. Get transaction info first production env, then sandbox.

lib version

<dependency>
    <groupId>com.apple.itunes.storekit</groupId>
    <artifactId>app-store-server-library</artifactId>
    <version>1.0.0</version>
</dependency>

We submitted the application and was rejected. The reviewer wrote the following comment:

When validating receipts on your server, your server needs to be able to handle a production-signed app getting its receipts from Apple’s test environment. The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code “Sandbox receipt used in production,” you should validate against the test environment instead.

To meet the conditions of the reviewer, we initialize for each environment client and data verifier:

 AppStoreServerAPIClient sandboxClient =
                new AppStoreServerAPIClient(encodedKey, keyId, issuerId, bundleId, Environment.SANDBOX);
SignedDataVerifier sandboxDataVerifier = new SignedDataVerifier(rootCAs, bundleId, appAppleId, Environment.SANDBOX, false);
               
 AppStoreServerAPIClient prodClient =
                new AppStoreServerAPIClient(encodedKey, keyId, issuerId, bundleId, Environment.PRODUCTION);
SignedDataVerifier prodDataVerifier = new SignedDataVerifier(rootCAs, bundleId, appAppleId, Environment.PRODUCTION, false);

                

and implemented the following logic

       try {
            getAllSubscriptionStatuses(prodClient, prodDataVerifier, originalTransactionId, subscriptionGroupId)
        } catch (e: APIException) {
            getAllSubscriptionStatuses(
                sandboxClient,
                sandboxDataVerifier,
                originalTransactionId,
                subscriptionGroupId
            )
                

I have a couple of questions:

  1. If this is a requirement from Apple, maybe this logic should be in your library? So that your customers don't think about it.
  2. The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code “Sandbox receipt used in production,” you should validate against the test environment instead.

In this case the library returns

{
  “errorCode” : 4040010,
  “errorMessage” : “Transaction id not found.”
}

This error matches enum constant com.apple.itunes.storekit.client.APIError#TRANSACTION_ID_NOT_FOUND
It's ok?

@dvcer

Complete Documentation for APIError Enum Values

Problem Statement

While working with the App Store Server API and handling various errors in Swift, I noticed that the APIError enum within the App Store Server Library lacks detailed documentation for some of its values. This makes it challenging for developers to understand the context and potential solutions for the errors returned by the API.

Proposed Solution

I propose to enhance the APIError enum by adding comprehensive documentation to each enum value. This documentation will include:

A brief description of the error.
A direct link to the official Apple documentation for each specific error (where available).
Suggestions for handling these errors within apps (if applicable).
For example, for the error INVALID_IN_APP_OWNERSHIP_TYPE, the documentation could look like this:

/**
 * An error that indicates the ownership type of an in-app purchase is invalid.
 *
 * This error occurs when the specified ownership type does not match any of the ownership types known by the App Store. To resolve this error, ensure that the ownership type corresponds to one of the valid types described in the App Store Server API documentation.
 *
 * @see <a href="https://developer.apple.com/documentation/appstoreserverapi/invalidinappownershiptypeerror">InvalidInAppOwnershipTypeError</a>
 */
`INVALID_IN_APP_OWNERSHIP_TYPE(4000026L),

Benefits

  • Improved Developer Experience: With detailed documentation directly in the source code, developers can more easily understand the meaning of each APIError and how to handle it effectively.
  • Quick Reference: Including direct links to the official documentation within the enum provides a handy reference, reducing the need to search through external documentation.
  • Best Practices: Where applicable, providing suggestions for error handling encourages best practices and more robust error management in apps using the App Store Server API.

Next Steps

I am prepared to undertake this enhancement by completing the documentation for each enum value in the APIError class. I believe this improvement will greatly benefit the developer community by making error handling more transparent and accessible.

Recommend Projects

  • React photo React

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

  • Vue.js photo Vue.js

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

  • Typescript photo Typescript

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

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

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

Recommend Topics

  • javascript

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

  • web

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

  • server

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

  • Machine learning

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

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

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

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.