Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for App Store Server API v1.11 and App Store Server Notif… #94

Merged
merged 1 commit into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -282,8 +282,16 @@ public enum APIError {
*
* @see <a href="https://developer.apple.com/documentation/appstoreserverapi/invalidtransactionnotconsumableerror">InvalidTransactionNotConsumableError</a>
*/
@Deprecated
INVALID_TRANSACTION_NOT_CONSUMABLE(4000043L),

/**
* An error that indicates the transaction identifier represents an unsupported in-app purchase type.
*
* @see <a href="https://developer.apple.com/documentation/appstoreserverapi/invalidtransactiontypenotsupportederror">InvalidTransactionTypeNotSupportedError</a>
*/
INVALID_TRANSACTION_TYPE_NOT_SUPPORTED(4000047L),

/**
* 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
Expand Up @@ -24,6 +24,7 @@ public class ConsumptionRequest {
private static final String SERIALIZED_NAME_LIFETIME_DOLLARS_REFUNDED = "lifetimeDollarsRefunded";
private static final String SERIALIZED_NAME_LIFETIME_DOLLARS_PURCHASED = "lifetimeDollarsPurchased";
private static final String SERIALIZED_NAME_USER_STATUS = "userStatus";
private static final String SERIALIZED_NAME_REFUND_PREFERENCE = "refundPreference";
@JsonProperty(SERIALIZED_NAME_CUSTOMER_CONSENTED)
private Boolean customerConsented;
@JsonProperty(SERIALIZED_NAME_CONSUMPTION_STATUS)
Expand All @@ -46,6 +47,8 @@ public class ConsumptionRequest {
private Integer lifetimeDollarsPurchased;
@JsonProperty(SERIALIZED_NAME_USER_STATUS)
private Integer userStatus;
@JsonProperty(SERIALIZED_NAME_REFUND_PREFERENCE)
private Integer refundPreference;


public ConsumptionRequest() {
Expand Down Expand Up @@ -348,6 +351,36 @@ public void setRawUserStatus(Integer rawUserStatus) {
this.userStatus = rawUserStatus;
}

public ConsumptionRequest refundPreference(RefundPreference refundPreference) {
this.refundPreference = refundPreference != null ? refundPreference.getValue() : null;
return this;
}

/**
* A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund.
*
* @return refundPreference
* @see <a href="https://developer.apple.com/documentation/appstoreserverapi/refundpreference">refundPreference</a>
**/
public RefundPreference getRefundPreference() {
return refundPreference != null ? RefundPreference.fromValue(refundPreference) : null;
}

/**
* @see #getRefundPreference()
*/
public Integer getRawRefundPreference() {
return refundPreference;
}

public void setRefundPreference(RefundPreference refundPreference) {
this.refundPreference = refundPreference != null ? refundPreference.getValue() : null;
}

public void setRawRefundPreference(Integer rawRefundPreference) {
this.refundPreference = rawRefundPreference;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -367,12 +400,13 @@ public boolean equals(Object o) {
Objects.equals(this.playTime, consumptionRequest.playTime) &&
Objects.equals(this.lifetimeDollarsRefunded, consumptionRequest.lifetimeDollarsRefunded) &&
Objects.equals(this.lifetimeDollarsPurchased, consumptionRequest.lifetimeDollarsPurchased) &&
Objects.equals(this.userStatus, consumptionRequest.userStatus);
Objects.equals(this.userStatus, consumptionRequest.userStatus) &&
Objects.equals(this.refundPreference, consumptionRequest.refundPreference);
}

@Override
public int hashCode() {
return Objects.hash(customerConsented, consumptionStatus, platform, sampleContentProvided, deliveryStatus, appAccountToken, accountTenure, playTime, lifetimeDollarsRefunded, lifetimeDollarsPurchased, userStatus);
return Objects.hash(customerConsented, consumptionStatus, platform, sampleContentProvided, deliveryStatus, appAccountToken, accountTenure, playTime, lifetimeDollarsRefunded, lifetimeDollarsPurchased, userStatus, refundPreference);
}

@Override
Expand All @@ -389,6 +423,7 @@ public String toString() {
", lifetimeDollarsRefunded=" + lifetimeDollarsRefunded +
", lifetimeDollarsPurchased=" + lifetimeDollarsPurchased +
", userStatus=" + userStatus +
", refundPreference=" + refundPreference +
'}';
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2024 Apple Inc. Licensed under MIT License.

package com.apple.itunes.storekit.model;

import com.fasterxml.jackson.annotation.JsonValue;

/**
* The customer-provided reason for a refund request.
*
* @see <a href="https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason">consumptionRequestReason</a>
*/
public enum ConsumptionRequestReason {

UNINTENDED_PURCHASE("UNINTENDED_PURCHASE"),
FULFILLMENT_ISSUE("FULFILLMENT_ISSUE"),
UNSATISFIED_WITH_PURCHASE("UNSATISFIED_WITH_PURCHASE"),
LEGAL("LEGAL"),
OTHER("OTHER");

private final String value;

ConsumptionRequestReason(String value) {
this.value = value;
}

public static ConsumptionRequestReason fromValue(String value) {
for (ConsumptionRequestReason b : ConsumptionRequestReason.values()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Why the letter b?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

B cause, no I think it is an artifact of the copy-paste from other enums.

if (b.value.equals(value)) {
return b;
}
}
return null;
}

@JsonValue
public String getValue() {
return value;
}

@Override
public String toString() {
return String.valueOf(value);
}
}

37 changes: 36 additions & 1 deletion src/main/java/com/apple/itunes/storekit/model/Data.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class Data {
private static final String SERIALIZED_NAME_SIGNED_TRANSACTION_INFO = "signedTransactionInfo";
private static final String SERIALIZED_NAME_SIGNED_RENEWAL_INFO = "signedRenewalInfo";
private static final String SERIALIZED_NAME_STATUS = "status";
private static final String SERIALIZED_NAME_CONSUMPTION_REQUEST_REASON = "consumptionRequestReason";
@JsonProperty(SERIALIZED_NAME_ENVIRONMENT)
private String environment;
@JsonProperty(SERIALIZED_NAME_APP_APPLE_ID)
Expand All @@ -35,6 +36,8 @@ public class Data {
private String signedRenewalInfo;
@JsonProperty(SERIALIZED_NAME_STATUS)
private Integer status;
@JsonProperty(SERIALIZED_NAME_CONSUMPTION_REQUEST_REASON)
private String consumptionRequestReason;
@JsonAnySetter
private Map<String, Object> unknownFields;

Expand Down Expand Up @@ -197,6 +200,36 @@ public void setRawStatus(Integer rawStatus) {
this.status = rawStatus;
}

public Data consumptionRequestReason(ConsumptionRequestReason consumptionRequestReason) {
this.consumptionRequestReason = consumptionRequestReason != null ? consumptionRequestReason.getValue() : null;
return this;
}

/**
* The reason the customer requested the refund.
*
* @return consumptionRequestReason
* @see <a href="https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason">consumptionRequestReason</a>
**/
public ConsumptionRequestReason getConsumptionRequestReason() {
return consumptionRequestReason != null ? ConsumptionRequestReason.fromValue(consumptionRequestReason) : null;
}

/**
* @see #getConsumptionRequestReason()
*/
public String getRawConsumptionRequestReason() {
return consumptionRequestReason;
}

public void setConsumptionRequestReason(ConsumptionRequestReason consumptionRequestReason) {
this.consumptionRequestReason = consumptionRequestReason != null ? consumptionRequestReason.getValue() : null;
}

public void setRawConsumptionRequestReason(String rawConsumptionRequestReason) {
this.consumptionRequestReason = rawConsumptionRequestReason;
}

public Data unknownFields(Map<String, Object> unknownFields) {
this.unknownFields = unknownFields;
return this;
Expand Down Expand Up @@ -231,12 +264,13 @@ public boolean equals(Object o) {
Objects.equals(this.signedTransactionInfo, data.signedTransactionInfo) &&
Objects.equals(this.signedRenewalInfo, data.signedRenewalInfo) &&
Objects.equals(this.status, data.status) &&
Objects.equals(this.consumptionRequestReason, data.consumptionRequestReason) &&
Objects.equals(this.unknownFields, data.unknownFields);
}

@Override
public int hashCode() {
return Objects.hash(environment, appAppleId, bundleId, bundleVersion, signedTransactionInfo, signedRenewalInfo, status, unknownFields);
return Objects.hash(environment, appAppleId, bundleId, bundleVersion, signedTransactionInfo, signedRenewalInfo, status, consumptionRequestReason, unknownFields);
}

@Override
Expand All @@ -249,6 +283,7 @@ public String toString() {
", signedTransactionInfo='" + signedTransactionInfo + '\'' +
", signedRenewalInfo='" + signedRenewalInfo + '\'' +
", status=" + status +
", consumptionRequestReason='" + consumptionRequestReason + '\'' +
", unknownFields=" + unknownFields +
'}';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2024 Apple Inc. Licensed under MIT License.

package com.apple.itunes.storekit.model;

import com.fasterxml.jackson.annotation.JsonValue;

/**
* A value that indicates your preferred outcome for the refund request.
*
* @see <a href="https://developer.apple.com/documentation/appstoreserverapi/refundpreference">refundPreference</a>
*/
public enum RefundPreference {

UNDECLARED(0),
PREFER_GRANT(1),
PREFER_DECLINE(2),
NO_PREFERENCE(3);

private final Integer value;

RefundPreference(Integer value) {
this.value = value;
}

public static RefundPreference fromValue(Integer value) {
for (RefundPreference b : RefundPreference.values()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

if (b.value.equals(value)) {
return b;
}
}
return null;
}

@JsonValue
public Integer getValue() {
return value;
}

@Override
public String toString() {
return String.valueOf(value);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.apple.itunes.storekit.model.Platform;
import com.apple.itunes.storekit.model.PlayTime;
import com.apple.itunes.storekit.model.RefundHistoryResponse;
import com.apple.itunes.storekit.model.RefundPreference;
import com.apple.itunes.storekit.model.SendAttemptItem;
import com.apple.itunes.storekit.model.SendAttemptResult;
import com.apple.itunes.storekit.model.SendTestNotificationResponse;
Expand Down Expand Up @@ -429,6 +430,7 @@ public void testSendConsumptionData() throws APIException, IOException {
Assertions.assertEquals(6, ((Number) root.get("lifetimeDollarsRefunded")).intValue());
Assertions.assertEquals(7, ((Number) root.get("lifetimeDollarsPurchased")).intValue());
Assertions.assertEquals(4, ((Number) root.get("userStatus")).intValue());
Assertions.assertEquals(3, ((Number) root.get("refundPreference")).intValue());
});

ConsumptionRequest consumptionRequest = new ConsumptionRequest()
Expand All @@ -442,7 +444,8 @@ public void testSendConsumptionData() throws APIException, IOException {
.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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,37 @@ public void testNotificationDecoding() throws IOException, NoSuchAlgorithmExcept
Assertions.assertEquals("signed_renewal_info_value", notification.getData().getSignedRenewalInfo());
Assertions.assertEquals(Status.ACTIVE, notification.getData().getStatus());
Assertions.assertEquals(1, notification.getData().getRawStatus());
Assertions.assertNull(notification.getData().getConsumptionRequestReason());
Assertions.assertNull(notification.getData().getRawConsumptionRequestReason());
}

@Test
public void testConsumptionRequestNotificationDecoding() throws IOException, NoSuchAlgorithmException, VerificationException {
String signedNotification = SignedDataCreator.createSignedDataFromJson("models/signedConsumptionRequestNotification.json");

ResponseBodyV2DecodedPayload notification = TestingUtility.getSignedPayloadVerifier().verifyAndDecodeNotification(signedNotification);

Assertions.assertEquals(NotificationTypeV2.CONSUMPTION_REQUEST, notification.getNotificationType());
Assertions.assertEquals("CONSUMPTION_REQUEST", notification.getRawNotificationType());
Assertions.assertNull(notification.getSubtype());
Assertions.assertNull(notification.getRawSubtype());
Assertions.assertEquals("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.getNotificationUUID());
Assertions.assertEquals("2.0", notification.getVersion());
Assertions.assertEquals(1698148900000L, notification.getSignedDate());
Assertions.assertNotNull(notification.getData());
Assertions.assertNull(notification.getSummary());
Assertions.assertNull(notification.getExternalPurchaseToken());
Assertions.assertEquals(Environment.LOCAL_TESTING, notification.getData().getEnvironment());
Assertions.assertEquals("LocalTesting", notification.getData().getRawEnvironment());
Assertions.assertEquals(41234L, notification.getData().getAppAppleId());
Assertions.assertEquals("com.example", notification.getData().getBundleId());
Assertions.assertEquals("1.2.3", notification.getData().getBundleVersion());
Assertions.assertEquals("signed_transaction_info_value", notification.getData().getSignedTransactionInfo());
Assertions.assertEquals("signed_renewal_info_value", notification.getData().getSignedRenewalInfo());
Assertions.assertEquals(Status.ACTIVE, notification.getData().getStatus());
Assertions.assertEquals(1, notification.getData().getRawStatus());
Assertions.assertEquals(ConsumptionRequestReason.UNINTENDED_PURCHASE, notification.getData().getConsumptionRequestReason());
Assertions.assertEquals("UNINTENDED_PURCHASE", notification.getData().getRawConsumptionRequestReason());
}

@Test
Expand Down
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
}
Loading