Skip to content

Commit

Permalink
Merge pull request #152 from alexanderjordanbaker/ASSA1.15andASSN2.15
Browse files Browse the repository at this point in the history
Update to support App Store Server API 1.15 and App Store Server …
  • Loading branch information
alexanderjordanbaker authored Mar 8, 2025
2 parents 25321f1 + 19a2df2 commit 681cbd2
Show file tree
Hide file tree
Showing 21 changed files with 714 additions and 75 deletions.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
14 changes: 7 additions & 7 deletions gradlew
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,15 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
Expand Down Expand Up @@ -202,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.

set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

package com.apple.itunes.storekit.advancedcommerce;

import com.apple.itunes.storekit.model.AdvancedCommerceInAppRequest;
import com.apple.itunes.storekit.signature.JWSSignatureCreator;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class AdvancedCommerceInAppSignatureCreator extends JWSSignatureCreator {

private static final String AUDIENCE = "advanced-commerce-api";
private static final String REQUEST_KEY = "request";

private final ObjectMapper objectMapper;

/**
* Create an AdvancedCommerceInAppSignatureCreator
* @param signingKey Your private key downloaded from App Store Connect
* @param keyId Your private key ID from App Store Connect
* @param issuerId Your issuer ID from the Keys page in App Store Connect
* @param bundleId Your app's bundle ID
*/
public AdvancedCommerceInAppSignatureCreator(String signingKey, String keyId, String issuerId, String bundleId) {
super(AUDIENCE, signingKey, keyId, issuerId, bundleId);
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));
}

/**
* Create an Advanced Commerce in-app signed request.
*
* @see <a href="https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests">Generating JWS to sign App Store requests</a>
* @param advancedCommerceInAppRequest The request to be signed.
* @return The signed JWS
*/
public String createSignature(AdvancedCommerceInAppRequest advancedCommerceInAppRequest) throws IOException {
if (advancedCommerceInAppRequest == null) {
throw new IllegalArgumentException("advancedCommerceInAppRequest cannot be null");
}
String jsonRequest = objectMapper.writeValueAsString(advancedCommerceInAppRequest);
byte[] utf8Bytes = jsonRequest.getBytes(StandardCharsets.UTF_8);
String encodedRequest = Base64.getEncoder().encodeToString(utf8Bytes);
Map<String, Object> claims = new HashMap<>();
claims.put(REQUEST_KEY, encodedRequest);
return createSignature(claims);
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/apple/itunes/storekit/client/APIError.java
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,14 @@ public enum APIError {
*/
INVALID_TRANSACTION_TYPE_NOT_SUPPORTED(4000047L),


/**
* An error that indicates the endpoint doesn't support an app transaction ID.
*
* @see <a href="https://developer.apple.com/documentation/appstoreserverapi/apptransactionidnotsupportederror">AppTransactionIdNotSupportedError</a>
*/
APP_TRANSACTION_ID_NOT_SUPPORTED_ERROR(4000048L),

/**
* An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.apple.itunes.storekit.model;

public interface AdvancedCommerceInAppRequest {
}
61 changes: 59 additions & 2 deletions src/main/java/com/apple/itunes/storekit/model/AppTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class AppTransaction implements DecodedSignedData {
private static final String SERIALIZED_NAME_DEVICE_VERIFICATION = "deviceVerification";
private static final String SERIALIZED_NAME_DEVICE_VERIFICATION_NONCE = "deviceVerificationNonce";
private static final String SERIALIZED_NAME_PREORDER_DATE = "preorderDate";
private static final String SERIALIZED_NAME_APP_TRANSACTION_ID = "appTransactionId";
private static final String SERIALIZED_NAME_ORIGINAL_PLATFORM = "originalPlatform";

@JsonProperty(SERIALIZED_NAME_RECEIPT_TYPE)
private String receiptType;
Expand All @@ -52,6 +54,10 @@ public class AppTransaction implements DecodedSignedData {
@JsonProperty(SERIALIZED_NAME_PREORDER_DATE)
@JsonDeserialize(using = XcodeCompatibleTimestampDeserializer.class)
private Long preorderDate;
@JsonProperty(SERIALIZED_NAME_APP_TRANSACTION_ID)
private String appTransactionId;
@JsonProperty(SERIALIZED_NAME_ORIGINAL_PLATFORM)
private String originalPlatform;
@JsonAnySetter
private Map<String, Object> unknownFields;

Expand Down Expand Up @@ -265,6 +271,55 @@ public AppTransaction preorderDate(Long preorderDate) {
return this;
}

/**
* The unique identifier of the app download transaction.
*
* @return appTransactionId
* @see <a href="https://developer.apple.com/documentation/storekit/apptransaction/apptransactionid">appTransactionId</a>
**/
public String getAppTransactionId() {
return this.appTransactionId;
}

public void setAppTransactionId(String appTransactionId) {
this.appTransactionId = appTransactionId;
}

public AppTransaction appTransactionId(String appTransactionId) {
this.appTransactionId = appTransactionId;
return this;
}

public AppTransaction originalPlatform(PurchasePlatform originalPlatform) {
this.originalPlatform = originalPlatform != null ? originalPlatform.getValue() : null;
return this;
}

/**
* The platform on which the customer originally purchased the app.
*
* @return originalPlatform
* @see <a href="https://developer.apple.com/documentation/storekit/apptransaction/originalplatform-4mogz">originalPlatform</a>
**/
public PurchasePlatform getOriginalPlatform() {
return originalPlatform != null ? PurchasePlatform.fromValue(originalPlatform) : null;
}

/**
* @see #getOriginalPlatform()
*/
public String getRawOriginalPlatform() {
return originalPlatform;
}

public void setOriginalPlatform(PurchasePlatform originalPlatform) {
this.originalPlatform = originalPlatform != null ? originalPlatform.getValue() : null;
}

public void setRawOriginalPlatform(String rawOriginalPlatform) {
this.originalPlatform = rawOriginalPlatform;
}

/**
Fields that are not recognized for this object
Expand Down Expand Up @@ -292,12 +347,12 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AppTransaction that = (AppTransaction) o;
return Objects.equals(receiptType, that.receiptType) && Objects.equals(appAppleId, that.appAppleId) && Objects.equals(bundleId, that.bundleId) && Objects.equals(applicationVersion, that.applicationVersion) && Objects.equals(versionExternalIdentifier, that.versionExternalIdentifier) && Objects.equals(originalPurchaseDate, that.originalPurchaseDate) && Objects.equals(originalApplicationVersion, that.originalApplicationVersion) && Objects.equals(deviceVerification, that.deviceVerification) && Objects.equals(deviceVerificationNonce, that.deviceVerificationNonce) && Objects.equals(preorderDate, that.preorderDate) && Objects.equals(unknownFields, that.unknownFields);
return Objects.equals(receiptType, that.receiptType) && Objects.equals(appAppleId, that.appAppleId) && Objects.equals(bundleId, that.bundleId) && Objects.equals(applicationVersion, that.applicationVersion) && Objects.equals(versionExternalIdentifier, that.versionExternalIdentifier) && Objects.equals(receiptCreationDate, that.receiptCreationDate) && Objects.equals(originalPurchaseDate, that.originalPurchaseDate) && Objects.equals(originalApplicationVersion, that.originalApplicationVersion) && Objects.equals(deviceVerification, that.deviceVerification) && Objects.equals(deviceVerificationNonce, that.deviceVerificationNonce) && Objects.equals(preorderDate, that.preorderDate) && Objects.equals(appTransactionId, that.appTransactionId) && Objects.equals(originalPlatform, that.originalPlatform) && Objects.equals(unknownFields, that.unknownFields);
}

@Override
public int hashCode() {
return Objects.hash(receiptType, appAppleId, bundleId, applicationVersion, versionExternalIdentifier, originalPurchaseDate, originalApplicationVersion, deviceVerification, deviceVerificationNonce, preorderDate, unknownFields);
return Objects.hash(receiptType, appAppleId, bundleId, applicationVersion, versionExternalIdentifier, receiptCreationDate, originalPurchaseDate, originalApplicationVersion, deviceVerification, deviceVerificationNonce, preorderDate, appTransactionId, originalPlatform, unknownFields);
}

@Override
Expand All @@ -314,6 +369,8 @@ public String toString() {
", deviceVerification='" + deviceVerification + '\'' +
", deviceVerificationNonce=" + deviceVerificationNonce +
", preorderDate=" + preorderDate +
", appTransactionId='" + appTransactionId + '\'' +
", originalPlatform='" + originalPlatform + '\'' +
", unknownFields=" + unknownFields +
'}';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

/**
* A decoded payload containing subscription renewal information for an auto-renewable subscription.
Expand All @@ -35,6 +36,9 @@ public class JWSRenewalInfoDecodedPayload implements DecodedSignedData {
private static final String SERIALIZED_NAME_CURRENCY = "currency";
private static final String SERIALIZED_NAME_OFFER_DISCOUNT_TYPE = "offerDiscountType";
private static final String SERIALIZED_NAME_ELIGIBLE_WIN_BACK_OFFER_IDS = "eligibleWinBackOfferIds";
private static final String SERIALIZED_NAME_APP_TRANSACTION_ID = "appTransactionId";
private static final String SERIALIZED_NAME_OFFER_PERIOD = "offerPeriod";
private static final String SERIALIZED_NAME_APP_ACCOUNT_TOKEN = "appAccountToken";
@JsonProperty(SERIALIZED_NAME_EXPIRATION_INTENT)
private Integer expirationIntent;
@JsonProperty(SERIALIZED_NAME_ORIGINAL_TRANSACTION_ID)
Expand Down Expand Up @@ -75,6 +79,12 @@ public class JWSRenewalInfoDecodedPayload implements DecodedSignedData {
private String offerDiscountType;
@JsonProperty(SERIALIZED_NAME_ELIGIBLE_WIN_BACK_OFFER_IDS)
private List<String> eligibleWinBackOfferIds = null;
@JsonProperty(SERIALIZED_NAME_APP_TRANSACTION_ID)
private String appTransactionId;
@JsonProperty(SERIALIZED_NAME_OFFER_PERIOD)
private String offerPeriod;
@JsonProperty(SERIALIZED_NAME_APP_ACCOUNT_TOKEN)
private UUID appAccountToken;
@JsonAnySetter
private Map<String, Object> unknownFields;

Expand Down Expand Up @@ -498,6 +508,62 @@ public void setEligibleWinBackOfferIds(List<String> eligibleWinBackOfferIds) {
this.eligibleWinBackOfferIds = eligibleWinBackOfferIds;
}

public JWSRenewalInfoDecodedPayload appTransactionId(String appTransactionId) {
this.appTransactionId = appTransactionId;
return this;
}

/**
* The unique identifier of the app download transaction.
*
* @return appTransactionId
* @see <a href="https://developer.apple.com/documentation/appstoreserverapi/appTransactionId">appTransactionId</a>
**/
public String getAppTransactionId() {
return this.appTransactionId;
}

public void setAppTransactionId(String appTransactionId) {
this.appTransactionId = appTransactionId;
}

public JWSRenewalInfoDecodedPayload offerPeriod(String offerPeriod) {
this.offerPeriod = offerPeriod;
return this;
}

/**
* The duration of the offer.
*
* @return offerPeriod
* @see <a href="https://developer.apple.com/documentation/appstoreserverapi/offerPeriod">offerPeriod</a>
**/
public String getOfferPeriod() {
return this.offerPeriod;
}

public void setOfferPeriod(String offerPeriod) {
this.offerPeriod = offerPeriod;
}

public JWSRenewalInfoDecodedPayload appAccountToken(UUID appAccountToken) {
this.appAccountToken = appAccountToken;
return this;
}

/**
* The UUID that an app optionally generates to map a customer’s In-App Purchase with its resulting App Store transaction.
*
* @return appAccountToken
* @see <a href="https://developer.apple.com/documentation/appstoreserverapi/appAccountToken">appAccountToken</a>
**/
public UUID getAppAccountToken() {
return this.appAccountToken;
}

public void setAppAccountToken(UUID appAccountToken) {
this.appAccountToken = appAccountToken;
}

public JWSRenewalInfoDecodedPayload unknownFields(Map<String, Object> unknownFields) {
this.unknownFields = unknownFields;
Expand All @@ -519,37 +585,15 @@ public void setUnknownFields(Map<String, Object> unknownFields) {

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
JWSRenewalInfoDecodedPayload jwSRenewalInfoDecodedPayload = (JWSRenewalInfoDecodedPayload) o;
return Objects.equals(this.expirationIntent, jwSRenewalInfoDecodedPayload.expirationIntent) &&
Objects.equals(this.originalTransactionId, jwSRenewalInfoDecodedPayload.originalTransactionId) &&
Objects.equals(this.autoRenewProductId, jwSRenewalInfoDecodedPayload.autoRenewProductId) &&
Objects.equals(this.productId, jwSRenewalInfoDecodedPayload.productId) &&
Objects.equals(this.autoRenewStatus, jwSRenewalInfoDecodedPayload.autoRenewStatus) &&
Objects.equals(this.isInBillingRetryPeriod, jwSRenewalInfoDecodedPayload.isInBillingRetryPeriod) &&
Objects.equals(this.priceIncreaseStatus, jwSRenewalInfoDecodedPayload.priceIncreaseStatus) &&
Objects.equals(this.gracePeriodExpiresDate, jwSRenewalInfoDecodedPayload.gracePeriodExpiresDate) &&
Objects.equals(this.offerType, jwSRenewalInfoDecodedPayload.offerType) &&
Objects.equals(this.offerIdentifier, jwSRenewalInfoDecodedPayload.offerIdentifier) &&
Objects.equals(this.signedDate, jwSRenewalInfoDecodedPayload.signedDate) &&
Objects.equals(this.environment, jwSRenewalInfoDecodedPayload.environment) &&
Objects.equals(this.recentSubscriptionStartDate, jwSRenewalInfoDecodedPayload.recentSubscriptionStartDate) &&
Objects.equals(this.renewalDate, jwSRenewalInfoDecodedPayload.renewalDate) &&
Objects.equals(this.renewalPrice, jwSRenewalInfoDecodedPayload.renewalPrice) &&
Objects.equals(this.currency, jwSRenewalInfoDecodedPayload.currency) &&
Objects.equals(this.offerDiscountType, jwSRenewalInfoDecodedPayload.offerDiscountType) &&
Objects.equals(this.eligibleWinBackOfferIds, jwSRenewalInfoDecodedPayload.eligibleWinBackOfferIds) &&
Objects.equals(this.unknownFields, jwSRenewalInfoDecodedPayload.unknownFields);
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JWSRenewalInfoDecodedPayload that = (JWSRenewalInfoDecodedPayload) o;
return Objects.equals(expirationIntent, that.expirationIntent) && Objects.equals(originalTransactionId, that.originalTransactionId) && Objects.equals(autoRenewProductId, that.autoRenewProductId) && Objects.equals(productId, that.productId) && Objects.equals(autoRenewStatus, that.autoRenewStatus) && Objects.equals(isInBillingRetryPeriod, that.isInBillingRetryPeriod) && Objects.equals(priceIncreaseStatus, that.priceIncreaseStatus) && Objects.equals(gracePeriodExpiresDate, that.gracePeriodExpiresDate) && Objects.equals(offerType, that.offerType) && Objects.equals(offerIdentifier, that.offerIdentifier) && Objects.equals(signedDate, that.signedDate) && Objects.equals(environment, that.environment) && Objects.equals(recentSubscriptionStartDate, that.recentSubscriptionStartDate) && Objects.equals(renewalDate, that.renewalDate) && Objects.equals(renewalPrice, that.renewalPrice) && Objects.equals(currency, that.currency) && Objects.equals(offerDiscountType, that.offerDiscountType) && Objects.equals(eligibleWinBackOfferIds, that.eligibleWinBackOfferIds) && Objects.equals(appTransactionId, that.appTransactionId) && Objects.equals(offerPeriod, that.offerPeriod) && Objects.equals(appAccountToken, that.appAccountToken) && Objects.equals(unknownFields, that.unknownFields);
}

@Override
public int hashCode() {
return Objects.hash(expirationIntent, originalTransactionId, autoRenewProductId, productId, autoRenewStatus, isInBillingRetryPeriod, priceIncreaseStatus, gracePeriodExpiresDate, offerType, offerIdentifier, signedDate, environment, recentSubscriptionStartDate, renewalDate, renewalPrice, currency, offerDiscountType, eligibleWinBackOfferIds, unknownFields);
return Objects.hash(expirationIntent, originalTransactionId, autoRenewProductId, productId, autoRenewStatus, isInBillingRetryPeriod, priceIncreaseStatus, gracePeriodExpiresDate, offerType, offerIdentifier, signedDate, environment, recentSubscriptionStartDate, renewalDate, renewalPrice, currency, offerDiscountType, eligibleWinBackOfferIds, appTransactionId, offerPeriod, appAccountToken, unknownFields);
}

@Override
Expand All @@ -573,6 +617,9 @@ public String toString() {
", currency='" + currency + '\'' +
", offerDiscountType='" + offerDiscountType + '\'' +
", eligibleWinBackOfferIds=" + eligibleWinBackOfferIds +
", appTransactionId='" + appTransactionId + '\'' +
", offerPeriod='" + offerPeriod + '\'' +
", appAccountToken='" + appAccountToken + '\'' +
", unknownFields=" + unknownFields +
'}';
}
Expand Down
Loading

0 comments on commit 681cbd2

Please sign in to comment.