Apple In-App Purchase Implementation in Java

Overview

Apple In-App Purchase (IAP) enables monetization in iOS, macOS, and other Apple platform applications. This Java implementation provides server-side receipt validation, subscription management, and IAP processing.

1. Dependencies

<dependencies>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- JWT for App Store Server API -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<!-- Cryptography -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<!-- Date/Time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.12.5</version>
</dependency>
</dependencies>

2. Core Domain Models

Receipt and Purchase Models

package com.example.iap.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
public class AppStoreReceipt {
@JsonProperty("receipt")
private Receipt receipt;
@JsonProperty("latest_receipt")
private String latestReceipt;
@JsonProperty("latest_receipt_info")
private List<InAppPurchase> latestReceiptInfo;
@JsonProperty("pending_renewal_info")
private List<PendingRenewalInfo> pendingRenewalInfo;
@JsonProperty("environment")
private Environment environment;
@JsonProperty("status")
private int status;
// Getters and Setters
public Receipt getReceipt() { return receipt; }
public void setReceipt(Receipt receipt) { this.receipt = receipt; }
public String getLatestReceipt() { return latestReceipt; }
public void setLatestReceipt(String latestReceipt) { this.latestReceipt = latestReceipt; }
public List<InAppPurchase> getLatestReceiptInfo() { return latestReceiptInfo; }
public void setLatestReceiptInfo(List<InAppPurchase> latestReceiptInfo) { this.latestReceiptInfo = latestReceiptInfo; }
public List<PendingRenewalInfo> getPendingRenewalInfo() { return pendingRenewalInfo; }
public void setPendingRenewalInfo(List<PendingRenewalInfo> pendingRenewalInfo) { this.pendingRenewalInfo = pendingRenewalInfo; }
public Environment getEnvironment() { return environment; }
public void setEnvironment(Environment environment) { this.environment = environment; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
}
class Receipt {
@JsonProperty("receipt_type")
private String receiptType;
@JsonProperty("adam_id")
private long adamId;
@JsonProperty("app_item_id")
private long appItemId;
@JsonProperty("bundle_id")
private String bundleId;
@JsonProperty("application_version")
private String applicationVersion;
@JsonProperty("download_id")
private long downloadId;
@JsonProperty("version_external_identifier")
private long versionExternalIdentifier;
@JsonProperty("receipt_creation_date")
private String receiptCreationDate;
@JsonProperty("receipt_creation_date_ms")
private String receiptCreationDateMs;
@JsonProperty("receipt_creation_date_pst")
private String receiptCreationDatePst;
@JsonProperty("request_date")
private String requestDate;
@JsonProperty("request_date_ms")
private String requestDateMs;
@JsonProperty("request_date_pst")
private String requestDatePst;
@JsonProperty("original_purchase_date")
private String originalPurchaseDate;
@JsonProperty("original_purchase_date_ms")
private String originalPurchaseDateMs;
@JsonProperty("original_purchase_date_pst")
private String originalPurchaseDatePst;
@JsonProperty("original_application_version")
private String originalApplicationVersion;
@JsonProperty("in_app")
private List<InAppPurchase> inApp;
// Getters and Setters
public String getReceiptType() { return receiptType; }
public void setReceiptType(String receiptType) { this.receiptType = receiptType; }
public long getAdamId() { return adamId; }
public void setAdamId(long adamId) { this.adamId = adamId; }
public long getAppItemId() { return appItemId; }
public void setAppItemId(long appItemId) { this.appItemId = appItemId; }
public String getBundleId() { return bundleId; }
public void setBundleId(String bundleId) { this.bundleId = bundleId; }
public String getApplicationVersion() { return applicationVersion; }
public void setApplicationVersion(String applicationVersion) { this.applicationVersion = applicationVersion; }
public long getDownloadId() { return downloadId; }
public void setDownloadId(long downloadId) { this.downloadId = downloadId; }
public long getVersionExternalIdentifier() { return versionExternalIdentifier; }
public void setVersionExternalIdentifier(long versionExternalIdentifier) { this.versionExternalIdentifier = versionExternalIdentifier; }
public String getReceiptCreationDate() { return receiptCreationDate; }
public void setReceiptCreationDate(String receiptCreationDate) { this.receiptCreationDate = receiptCreationDate; }
public String getReceiptCreationDateMs() { return receiptCreationDateMs; }
public void setReceiptCreationDateMs(String receiptCreationDateMs) { this.receiptCreationDateMs = receiptCreationDateMs; }
public String getReceiptCreationDatePst() { return receiptCreationDatePst; }
public void setReceiptCreationDatePst(String receiptCreationDatePst) { this.receiptCreationDatePst = receiptCreationDatePst; }
public String getRequestDate() { return requestDate; }
public void setRequestDate(String requestDate) { this.requestDate = requestDate; }
public String getRequestDateMs() { return requestDateMs; }
public void setRequestDateMs(String requestDateMs) { this.requestDateMs = requestDateMs; }
public String getRequestDatePst() { return requestDatePst; }
public void setRequestDatePst(String requestDatePst) { this.requestDatePst = requestDatePst; }
public String getOriginalPurchaseDate() { return originalPurchaseDate; }
public void setOriginalPurchaseDate(String originalPurchaseDate) { this.originalPurchaseDate = originalPurchaseDate; }
public String getOriginalPurchaseDateMs() { return originalPurchaseDateMs; }
public void setOriginalPurchaseDateMs(String originalPurchaseDateMs) { this.originalPurchaseDateMs = originalPurchaseDateMs; }
public String getOriginalPurchaseDatePst() { return originalPurchaseDatePst; }
public void setOriginalPurchaseDatePst(String originalPurchaseDatePst) { this.originalPurchaseDatePst = receiptCreationDatePst; }
public String getOriginalApplicationVersion() { return originalApplicationVersion; }
public void setOriginalApplicationVersion(String originalApplicationVersion) { this.originalApplicationVersion = originalApplicationVersion; }
public List<InAppPurchase> getInApp() { return inApp; }
public void setInApp(List<InAppPurchase> inApp) { this.inApp = inApp; }
}
public class InAppPurchase {
@JsonProperty("quantity")
private String quantity;
@JsonProperty("product_id")
private String productId;
@JsonProperty("transaction_id")
private String transactionId;
@JsonProperty("original_transaction_id")
private String originalTransactionId;
@JsonProperty("purchase_date")
private String purchaseDate;
@JsonProperty("purchase_date_ms")
private String purchaseDateMs;
@JsonProperty("purchase_date_pst")
private String purchaseDatePst;
@JsonProperty("original_purchase_date")
private String originalPurchaseDate;
@JsonProperty("original_purchase_date_ms")
private String originalPurchaseDateMs;
@JsonProperty("original_purchase_date_pst")
private String originalPurchaseDatePst;
@JsonProperty("expires_date")
private String expiresDate;
@JsonProperty("expires_date_ms")
private String expiresDateMs;
@JsonProperty("expires_date_pst")
private String expiresDatePst;
@JsonProperty("web_order_line_item_id")
private String webOrderLineItemId;
@JsonProperty("is_trial_period")
private String isTrialPeriod;
@JsonProperty("is_in_intro_offer_period")
private String isInIntroOfferPeriod;
@JsonProperty("in_app_ownership_type")
private String ownershipType;
@JsonProperty("subscription_group_identifier")
private String subscriptionGroupIdentifier;
@JsonProperty("cancellation_date")
private String cancellationDate;
@JsonProperty("cancellation_date_ms")
private String cancellationDateMs;
@JsonProperty("cancellation_date_pst")
private String cancellationDatePst;
@JsonProperty("cancellation_reason")
private String cancellationReason;
// Getters and Setters
public String getQuantity() { return quantity; }
public void setQuantity(String quantity) { this.quantity = quantity; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getTransactionId() { return transactionId; }
public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
public String getOriginalTransactionId() { return originalTransactionId; }
public void setOriginalTransactionId(String originalTransactionId) { this.originalTransactionId = originalTransactionId; }
public String getPurchaseDate() { return purchaseDate; }
public void setPurchaseDate(String purchaseDate) { this.purchaseDate = purchaseDate; }
public String getPurchaseDateMs() { return purchaseDateMs; }
public void setPurchaseDateMs(String purchaseDateMs) { this.purchaseDateMs = purchaseDateMs; }
public String getPurchaseDatePst() { return purchaseDatePst; }
public void setPurchaseDatePst(String purchaseDatePst) { this.purchaseDatePst = purchaseDatePst; }
public String getOriginalPurchaseDate() { return originalPurchaseDate; }
public void setOriginalPurchaseDate(String originalPurchaseDate) { this.originalPurchaseDate = originalPurchaseDate; }
public String getOriginalPurchaseDateMs() { return originalPurchaseDateMs; }
public void setOriginalPurchaseDateMs(String originalPurchaseDateMs) { this.originalPurchaseDateMs = originalPurchaseDateMs; }
public String getOriginalPurchaseDatePst() { return originalPurchaseDatePst; }
public void setOriginalPurchaseDatePst(String originalPurchaseDatePst) { this.originalPurchaseDatePst = originalPurchaseDatePst; }
public String getExpiresDate() { return expiresDate; }
public void setExpiresDate(String expiresDate) { this.expiresDate = expiresDate; }
public String getExpiresDateMs() { return expiresDateMs; }
public void setExpiresDateMs(String expiresDateMs) { this.expiresDateMs = expiresDateMs; }
public String getExpiresDatePst() { return expiresDatePst; }
public void setExpiresDatePst(String expiresDatePst) { this.expiresDatePst = expiresDatePst; }
public String getWebOrderLineItemId() { return webOrderLineItemId; }
public void setWebOrderLineItemId(String webOrderLineItemId) { this.webOrderLineItemId = webOrderLineItemId; }
public String getIsTrialPeriod() { return isTrialPeriod; }
public void setIsTrialPeriod(String isTrialPeriod) { this.isTrialPeriod = isTrialPeriod; }
public String getIsInIntroOfferPeriod() { return isInIntroOfferPeriod; }
public void setIsInIntroOfferPeriod(String isInIntroOfferPeriod) { this.isInIntroOfferPeriod = isInIntroOfferPeriod; }
public String getOwnershipType() { return ownershipType; }
public void setOwnershipType(String ownershipType) { this.ownershipType = ownershipType; }
public String getSubscriptionGroupIdentifier() { return subscriptionGroupIdentifier; }
public void setSubscriptionGroupIdentifier(String subscriptionGroupIdentifier) { this.subscriptionGroupIdentifier = subscriptionGroupIdentifier; }
public String getCancellationDate() { return cancellationDate; }
public void setCancellationDate(String cancellationDate) { this.cancellationDate = cancellationDate; }
public String getCancellationDateMs() { return cancellationDateMs; }
public void setCancellationDateMs(String cancellationDateMs) { this.cancellationDateMs = cancellationDateMs; }
public String getCancellationDatePst() { return cancellationDatePst; }
public void setCancellationDatePst(String cancellationDatePst) { this.cancellationDatePst = cancellationDatePst; }
public String getCancellationReason() { return cancellationReason; }
public void setCancellationReason(String cancellationReason) { this.cancellationReason = cancellationReason; }
}
class PendingRenewalInfo {
@JsonProperty("auto_renew_product_id")
private String autoRenewProductId;
@JsonProperty("auto_renew_status")
private String autoRenewStatus;
@JsonProperty("expiration_intent")
private String expirationIntent;
@JsonProperty("grace_period_expires_date")
private String gracePeriodExpiresDate;
@JsonProperty("grace_period_expires_date_ms")
private String gracePeriodExpiresDateMs;
@JsonProperty("grace_period_expires_date_pst")
private String gracePeriodExpiresDatePst;
@JsonProperty("is_in_billing_retry_period")
private String isInBillingRetryPeriod;
@JsonProperty("offer_code_ref_name")
private String offerCodeRefName;
@JsonProperty("original_transaction_id")
private String originalTransactionId;
@JsonProperty("price_consent_status")
private String priceConsentStatus;
@JsonProperty("product_id")
private String productId;
@JsonProperty("promotional_offer_id")
private String promotionalOfferId;
// Getters and Setters
public String getAutoRenewProductId() { return autoRenewProductId; }
public void setAutoRenewProductId(String autoRenewProductId) { this.autoRenewProductId = autoRenewProductId; }
public String getAutoRenewStatus() { return autoRenewStatus; }
public void setAutoRenewStatus(String autoRenewStatus) { this.autoRenewStatus = autoRenewStatus; }
public String getExpirationIntent() { return expirationIntent; }
public void setExpirationIntent(String expirationIntent) { this.expirationIntent = expirationIntent; }
public String getGracePeriodExpiresDate() { return gracePeriodExpiresDate; }
public void setGracePeriodExpiresDate(String gracePeriodExpiresDate) { this.gracePeriodExpiresDate = gracePeriodExpiresDate; }
public String getGracePeriodExpiresDateMs() { return gracePeriodExpiresDateMs; }
public void setGracePeriodExpiresDateMs(String gracePeriodExpiresDateMs) { this.gracePeriodExpiresDateMs = gracePeriodExpiresDateMs; }
public String getGracePeriodExpiresDatePst() { return gracePeriodExpiresDatePst; }
public void setGracePeriodExpiresDatePst(String gracePeriodExpiresDatePst) { this.gracePeriodExpiresDatePst = gracePeriodExpiresDatePst; }
public String getIsInBillingRetryPeriod() { return isInBillingRetryPeriod; }
public void setIsInBillingRetryPeriod(String isInBillingRetryPeriod) { this.isInBillingRetryPeriod = isInBillingRetryPeriod; }
public String getOfferCodeRefName() { return offerCodeRefName; }
public void setOfferCodeRefName(String offerCodeRefName) { this.offerCodeRefName = offerCodeRefName; }
public String getOriginalTransactionId() { return originalTransactionId; }
public void setOriginalTransactionId(String originalTransactionId) { this.originalTransactionId = originalTransactionId; }
public String getPriceConsentStatus() { return priceConsentStatus; }
public void setPriceConsentStatus(String priceConsentStatus) { this.priceConsentStatus = priceConsentStatus; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getPromotionalOfferId() { return promotionalOfferId; }
public void setPromotionalOfferId(String promotionalOfferId) { this.promotionalOfferId = promotionalOfferId; }
}
public enum Environment {
PRODUCTION("Production"),
SANDBOX("Sandbox");
private final String value;
Environment(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public static Environment fromString(String value) {
for (Environment env : values()) {
if (env.value.equalsIgnoreCase(value)) {
return env;
}
}
return PRODUCTION;
}
}

Product and Subscription Models

package com.example.iap.model;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Map;
public class IAPProduct {
private String productId;
private ProductType type;
private String title;
private String description;
private BigDecimal price;
private String currency;
private Map<String, Object> metadata;
private boolean active;
// Getters and Setters
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public ProductType getType() { return type; }
public void setType(ProductType type) { this.type = type; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
public Map<String, Object> getMetadata() { return metadata; }
public void setMetadata(Map<String, Object> metadata) { this.metadata = metadata; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
}
public class UserSubscription {
private String userId;
private String originalTransactionId;
private String productId;
private ProductType productType;
private SubscriptionStatus status;
private LocalDateTime purchaseDate;
private LocalDateTime expiresDate;
private LocalDateTime cancellationDate;
private boolean autoRenewEnabled;
private String environment;
private boolean isTrialPeriod;
private boolean isInIntroOfferPeriod;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Getters and Setters
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getOriginalTransactionId() { return originalTransactionId; }
public void setOriginalTransactionId(String originalTransactionId) { this.originalTransactionId = originalTransactionId; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public ProductType getProductType() { return productType; }
public void setProductType(ProductType productType) { this.productType = productType; }
public SubscriptionStatus getStatus() { return status; }
public void setStatus(SubscriptionStatus status) { this.status = status; }
public LocalDateTime getPurchaseDate() { return purchaseDate; }
public void setPurchaseDate(LocalDateTime purchaseDate) { this.purchaseDate = purchaseDate; }
public LocalDateTime getExpiresDate() { return expiresDate; }
public void setExpiresDate(LocalDateTime expiresDate) { this.expiresDate = expiresDate; }
public LocalDateTime getCancellationDate() { return cancellationDate; }
public void setCancellationDate(LocalDateTime cancellationDate) { this.cancellationDate = cancellationDate; }
public boolean isAutoRenewEnabled() { return autoRenewEnabled; }
public void setAutoRenewEnabled(boolean autoRenewEnabled) { this.autoRenewEnabled = autoRenewEnabled; }
public String getEnvironment() { return environment; }
public void setEnvironment(String environment) { this.environment = environment; }
public boolean isTrialPeriod() { return isTrialPeriod; }
public void setTrialPeriod(boolean trialPeriod) { isTrialPeriod = trialPeriod; }
public boolean isInIntroOfferPeriod() { return isInIntroOfferPeriod; }
public void setInIntroOfferPeriod(boolean inIntroOfferPeriod) { isInIntroOfferPeriod = inIntroOfferPeriod; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public boolean isActive() {
if (status == SubscriptionStatus.ACTIVE || status == SubscriptionStatus.GRACE_PERIOD) {
return expiresDate == null || expiresDate.isAfter(LocalDateTime.now());
}
return false;
}
}
public enum ProductType {
CONSUMABLE("consumable"),
NON_CONSUMABLE("non_consumable"),
AUTO_RENEWABLE_SUBSCRIPTION("auto_renewable_subscription"),
NON_RENEWABLE_SUBSCRIPTION("non_renewable_subscription");
private final String value;
ProductType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public static ProductType fromString(String value) {
for (ProductType type : values()) {
if (type.value.equalsIgnoreCase(value)) {
return type;
}
}
return NON_CONSUMABLE;
}
}
public enum SubscriptionStatus {
ACTIVE("active"),
EXPIRED("expired"),
CANCELLED("cancelled"),
GRACE_PERIOD("grace_period"),
BILLING_RETRY("billing_retry"),
IN_TRIAL("in_trial"),
IN_INTRO_OFFER("in_intro_offer");
private final String value;
SubscriptionStatus(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public static SubscriptionStatus fromString(String value) {
for (SubscriptionStatus status : values()) {
if (status.value.equalsIgnoreCase(value)) {
return status;
}
}
return EXPIRED;
}
}

3. App Store Receipt Validation

package com.example.iap.validation;
import com.example.iap.model.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
public class ReceiptValidator {
private static final Logger log = LoggerFactory.getLogger(ReceiptValidator.class);
private final String sharedSecret;
private final boolean useSandbox;
private final ObjectMapper objectMapper;
// Apple App Store endpoints
private static final String PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt";
private static final String SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt";
public ReceiptValidator(String sharedSecret, boolean useSandbox) {
this.sharedSecret = sharedSecret;
this.useSandbox = useSandbox;
this.objectMapper = new ObjectMapper();
}
public ReceiptValidationResult validateReceipt(String receiptData) throws ReceiptValidationException {
return validateReceipt(receiptData, true);
}
public ReceiptValidationResult validateReceipt(String receiptData, boolean excludeOldTransactions) 
throws ReceiptValidationException {
try {
// First attempt with production environment
String url = useSandbox ? SANDBOX_URL : PRODUCTION_URL;
AppStoreReceipt response = sendValidationRequest(receiptData, url, excludeOldTransactions);
// If received 21007 status, retry with sandbox
if (response.getStatus() == 21007) {
log.info("Receipt from sandbox environment, retrying with sandbox URL");
response = sendValidationRequest(receiptData, SANDBOX_URL, excludeOldTransactions);
}
return processValidationResponse(response);
} catch (Exception e) {
throw new ReceiptValidationException("Failed to validate receipt", e);
}
}
private AppStoreReceipt sendValidationRequest(String receiptData, String url, boolean excludeOldTransactions) 
throws Exception {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-Type", "application/json");
// Build request payload
Map<String, Object> requestPayload = new HashMap<>();
requestPayload.put("receipt-data", receiptData);
requestPayload.put("password", sharedSecret);
requestPayload.put("exclude-old-transactions", excludeOldTransactions);
String requestBody = objectMapper.writeValueAsString(requestPayload);
httpPost.setEntity(new org.apache.hc.core5.http.io.entity.StringEntity(requestBody));
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String responseBody = EntityUtils.toString(response.getEntity());
if (response.getCode() != 200) {
throw new ReceiptValidationException("HTTP error during receipt validation: " + response.getCode());
}
return objectMapper.readValue(responseBody, AppStoreReceipt.class);
}
}
}
private ReceiptValidationResult processValidationResponse(AppStoreReceipt receipt) {
ReceiptValidationResult result = new ReceiptValidationResult();
result.setStatus(receipt.getStatus());
result.setEnvironment(receipt.getEnvironment());
if (receipt.getStatus() == 0) {
result.setValid(true);
result.setBundleId(receipt.getReceipt().getBundleId());
result.setApplicationVersion(receipt.getReceipt().getApplicationVersion());
// Process in-app purchases
List<InAppPurchase> allPurchases = new ArrayList<>();
if (receipt.getReceipt().getInApp() != null) {
allPurchases.addAll(receipt.getReceipt().getInApp());
}
if (receipt.getLatestReceiptInfo() != null) {
allPurchases.addAll(receipt.getLatestReceiptInfo());
}
result.setPurchases(processPurchases(allPurchases));
result.setPendingRenewalInfo(receipt.getPendingRenewalInfo());
} else {
result.setValid(false);
result.setErrorMessage(getStatusMessage(receipt.getStatus()));
}
return result;
}
private List<ProcessedPurchase> processPurchases(List<InAppPurchase> purchases) {
List<ProcessedPurchase> processed = new ArrayList<>();
for (InAppPurchase purchase : purchases) {
ProcessedPurchase processedPurchase = new ProcessedPurchase();
processedPurchase.setProductId(purchase.getProductId());
processedPurchase.setTransactionId(purchase.getTransactionId());
processedPurchase.setOriginalTransactionId(purchase.getOriginalTransactionId());
processedPurchase.setQuantity(Integer.parseInt(purchase.getQuantity()));
// Parse dates
if (purchase.getPurchaseDateMs() != null) {
processedPurchase.setPurchaseDate(parseAppleTimestamp(purchase.getPurchaseDateMs()));
}
if (purchase.getExpiresDateMs() != null) {
processedPurchase.setExpiresDate(parseAppleTimestamp(purchase.getExpiresDateMs()));
}
if (purchase.getCancellationDateMs() != null) {
processedPurchase.setCancellationDate(parseAppleTimestamp(purchase.getCancellationDateMs()));
}
processedPurchase.setTrialPeriod("true".equals(purchase.getIsTrialPeriod()));
processedPurchase.setIntroOfferPeriod("true".equals(purchase.getIsInIntroOfferPeriod()));
processedPurchase.setCancellationReason(purchase.getCancellationReason());
processedPurchase.setWebOrderLineItemId(purchase.getWebOrderLineItemId());
// Determine purchase type
processedPurchase.setProductType(determineProductType(purchase));
processed.add(processedPurchase);
}
return processed;
}
private ProductType determineProductType(InAppPurchase purchase) {
if (purchase.getExpiresDateMs() != null) {
return ProductType.AUTO_RENEWABLE_SUBSCRIPTION;
}
// Additional logic to determine other product types
return ProductType.NON_CONSUMABLE;
}
private LocalDateTime parseAppleTimestamp(String timestampMs) {
if (timestampMs == null) return null;
try {
long millis = Long.parseLong(timestampMs);
return LocalDateTime.ofInstant(
java.time.Instant.ofEpochMilli(millis), 
ZoneId.systemDefault()
);
} catch (NumberFormatException e) {
log.warn("Failed to parse Apple timestamp: {}", timestampMs);
return null;
}
}
private String getStatusMessage(int status) {
switch (status) {
case 0: return "Valid";
case 21000: return "The request to the App Store was not made using the HTTP POST request method.";
case 21001: return "The data in the receipt-data property was malformed or missing.";
case 21002: return "The receipt could not be authenticated.";
case 21003: return "The shared secret you provided does not match the shared secret on file for your account.";
case 21004: return "The receipt server is not currently available.";
case 21005: return "This receipt is valid but the subscription has expired.";
case 21006: return "This receipt is from the test environment, but it was sent to the production environment for verification.";
case 21007: return "This receipt is from the production environment, but it was sent to the test environment for verification.";
case 21008: return "This receipt could not be authorized. Treat this the same as if a purchase was never made.";
case 21009: return "Internal data access error.";
case 21010: return "The user account cannot be found or has been deleted.";
default: return "Unknown error";
}
}
public SubscriptionStatus determineSubscriptionStatus(ProcessedPurchase purchase, 
List<PendingRenewalInfo> pendingRenewals) {
LocalDateTime now = LocalDateTime.now();
// Check if cancelled
if (purchase.getCancellationDate() != null) {
return SubscriptionStatus.CANCELLED;
}
// Check if expired
if (purchase.getExpiresDate() != null && purchase.getExpiresDate().isBefore(now)) {
// Check for grace period or billing retry
Optional<PendingRenewalInfo> renewalInfo = pendingRenewals.stream()
.filter(r -> r.getOriginalTransactionId().equals(purchase.getOriginalTransactionId()))
.findFirst();
if (renewalInfo.isPresent()) {
PendingRenewalInfo info = renewalInfo.get();
// Check grace period
if (info.getGracePeriodExpiresDateMs() != null) {
LocalDateTime gracePeriodExpires = parseAppleTimestamp(info.getGracePeriodExpiresDateMs());
if (gracePeriodExpires != null && gracePeriodExpires.isAfter(now)) {
return SubscriptionStatus.GRACE_PERIOD;
}
}
// Check billing retry
if ("1".equals(info.getIsInBillingRetryPeriod())) {
return SubscriptionStatus.BILLING_RETRY;
}
}
return SubscriptionStatus.EXPIRED;
}
// Check if in trial or intro offer
if (purchase.isTrialPeriod()) {
return SubscriptionStatus.IN_TRIAL;
}
if (purchase.isIntroOfferPeriod()) {
return SubscriptionStatus.IN_INTRO_OFFER;
}
return SubscriptionStatus.ACTIVE;
}
}
class ReceiptValidationResult {
private boolean valid;
private int status;
private String errorMessage;
private Environment environment;
private String bundleId;
private String applicationVersion;
private List<ProcessedPurchase> purchases;
private List<PendingRenewalInfo> pendingRenewalInfo;
// Getters and Setters
public boolean isValid() { return valid; }
public void setValid(boolean valid) { this.valid = valid; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public String getErrorMessage() { return errorMessage; }
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
public Environment getEnvironment() { return environment; }
public void setEnvironment(Environment environment) { this.environment = environment; }
public String getBundleId() { return bundleId; }
public void setBundleId(String bundleId) { this.bundleId = bundleId; }
public String getApplicationVersion() { return applicationVersion; }
public void setApplicationVersion(String applicationVersion) { this.applicationVersion = applicationVersion; }
public List<ProcessedPurchase> getPurchases() { return purchases; }
public void setPurchases(List<ProcessedPurchase> purchases) { this.purchases = purchases; }
public List<PendingRenewalInfo> getPendingRenewalInfo() { return pendingRenewalInfo; }
public void setPendingRenewalInfo(List<PendingRenewalInfo> pendingRenewalInfo) { this.pendingRenewalInfo = pendingRenewalInfo; }
}
class ProcessedPurchase {
private String productId;
private String transactionId;
private String originalTransactionId;
private int quantity;
private LocalDateTime purchaseDate;
private LocalDateTime expiresDate;
private LocalDateTime cancellationDate;
private boolean isTrialPeriod;
private boolean isIntroOfferPeriod;
private String cancellationReason;
private String webOrderLineItemId;
private ProductType productType;
// Getters and Setters
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getTransactionId() { return transactionId; }
public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
public String getOriginalTransactionId() { return originalTransactionId; }
public void setOriginalTransactionId(String originalTransactionId) { this.originalTransactionId = originalTransactionId; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public LocalDateTime getPurchaseDate() { return purchaseDate; }
public void setPurchaseDate(LocalDateTime purchaseDate) { this.purchaseDate = purchaseDate; }
public LocalDateTime getExpiresDate() { return expiresDate; }
public void setExpiresDate(LocalDateTime expiresDate) { this.expiresDate = expiresDate; }
public LocalDateTime getCancellationDate() { return cancellationDate; }
public void setCancellationDate(LocalDateTime cancellationDate) { this.cancellationDate = cancellationDate; }
public boolean isTrialPeriod() { return isTrialPeriod; }
public void setTrialPeriod(boolean trialPeriod) { isTrialPeriod = trialPeriod; }
public boolean isIntroOfferPeriod() { return isIntroOfferPeriod; }
public void setIntroOfferPeriod(boolean introOfferPeriod) { isIntroOfferPeriod = introOfferPeriod; }
public String getCancellationReason() { return cancellationReason; }
public void setCancellationReason(String cancellationReason) { this.cancellationReason = cancellationReason; }
public String getWebOrderLineItemId() { return webOrderLineItemId; }
public void setWebOrderLineItemId(String webOrderLineItemId) { this.webOrderLineItemId = webOrderLineItemId; }
public ProductType getProductType() { return productType; }
public void setProductType(ProductType productType) { this.productType = productType; }
}
class ReceiptValidationException extends Exception {
public ReceiptValidationException(String message) {
super(message);
}
public ReceiptValidationException(String message, Throwable cause) {
super(message, cause);
}
}

4. App Store Server API Integration

package com.example.iap.api;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.example.iap.model.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Reader;
import java.io.StringReader;
import java.security.PrivateKey;
import java.security.interfaces.ECPrivateKey;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
public class AppStoreServerAPI {
private static final Logger log = LoggerFactory.getLogger(AppStoreServerAPI.class);
private final String issuerId;
private final String bundleId;
private final String keyId;
private final PrivateKey privateKey;
private final ObjectMapper objectMapper;
private static final String BASE_URL = "https://api.storekit.itunes.apple.com";
private static final String SANDBOX_BASE_URL = "https://api.storekit-sandbox.itunes.apple.com";
public AppStoreServerAPI(String issuerId, String bundleId, String keyId, String privateKey) 
throws AppStoreAPIException {
this.issuerId = issuerId;
this.bundleId = bundleId;
this.keyId = keyId;
this.privateKey = parsePrivateKey(privateKey);
this.objectMapper = new ObjectMapper();
}
public TransactionHistoryResponse getTransactionHistory(String originalTransactionId, boolean useSandbox) 
throws AppStoreAPIException {
return getTransactionHistory(originalTransactionId, null, null, 20, useSandbox);
}
public TransactionHistoryResponse getTransactionHistory(String originalTransactionId, String revision, 
String productTypes, int limit, boolean useSandbox) 
throws AppStoreAPIException {
String baseUrl = useSandbox ? SANDBOX_BASE_URL : BASE_URL;
String url = String.format("%s/inApps/v1/history/%s", baseUrl, originalTransactionId);
// Build query parameters
Map<String, String> params = new HashMap<>();
if (revision != null) params.put("revision", revision);
if (productTypes != null) params.put("productTypes", productTypes);
params.put("limit", String.valueOf(limit));
String queryString = buildQueryString(params);
if (!queryString.isEmpty()) {
url += "?" + queryString;
}
try {
String response = makeAuthenticatedRequest(url, "GET", null, useSandbox);
return objectMapper.readValue(response, TransactionHistoryResponse.class);
} catch (Exception e) {
throw new AppStoreAPIException("Failed to get transaction history", e);
}
}
public SubscriptionStatusResponse getSubscriptionStatus(String originalTransactionId, boolean useSandbox) 
throws AppStoreAPIException {
String baseUrl = useSandbox ? SANDBOX_BASE_URL : BASE_URL;
String url = String.format("%s/inApps/v1/subscriptions/%s", baseUrl, originalTransactionId);
try {
String response = makeAuthenticatedRequest(url, "GET", null, useSandbox);
return objectMapper.readValue(response, SubscriptionStatusResponse.class);
} catch (Exception e) {
throw new AppStoreAPIException("Failed to get subscription status", e);
}
}
public LookupOrderIdResponse lookupOrderId(String orderId, boolean useSandbox) throws AppStoreAPIException {
String baseUrl = useSandbox ? SANDBOX_BASE_URL : BASE_URL;
String url = String.format("%s/inApps/v1/lookup/%s", baseUrl, orderId);
try {
String response = makeAuthenticatedRequest(url, "GET", null, useSandbox);
return objectMapper.readValue(response, LookupOrderIdResponse.class);
} catch (Exception e) {
throw new AppStoreAPIException("Failed to lookup order ID", e);
}
}
public ExtendRenewalDateResponse extendRenewalDate(String originalTransactionId, 
ExtendRenewalDateRequest request, boolean useSandbox) 
throws AppStoreAPIException {
String baseUrl = useSandbox ? SANDBOX_BASE_URL : BASE_URL;
String url = String.format("%s/inApps/v1/subscriptions/extend/%s", baseUrl, originalTransactionId);
try {
String requestBody = objectMapper.writeValueAsString(request);
String response = makeAuthenticatedRequest(url, "PUT", requestBody, useSandbox);
return objectMapper.readValue(response, ExtendRenewalDateResponse.class);
} catch (Exception e) {
throw new AppStoreAPIException("Failed to extend renewal date", e);
}
}
private String makeAuthenticatedRequest(String url, String method, String body, boolean useSandbox) 
throws Exception {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(url);
String token = generateJWT();
httpGet.setHeader("Authorization", "Bearer " + token);
httpGet.setHeader("User-Agent", "YourApp/1.0");
if (body != null) {
httpGet.setHeader("Content-Type", "application/json");
// For POST/PUT requests, you would use HttpPost/HttpPut
}
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
String responseBody = EntityUtils.toString(response.getEntity());
if (response.getCode() != 200) {
throw new AppStoreAPIException("API request failed: " + response.getCode() + " - " + responseBody);
}
return responseBody;
}
}
}
private String generateJWT() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime expiration = now.plusMinutes(5); // Token expires in 5 minutes
Algorithm algorithm = Algorithm.ECDSA256(null, (ECPrivateKey) privateKey);
return JWT.create()
.withIssuer(issuerId)
.withIssuedAt(Date.from(now.toInstant(ZoneOffset.UTC)))
.withExpiresAt(Date.from(expiration.toInstant(ZoneOffset.UTC)))
.withAudience("appstoreconnect-v1")
.withSubject(bundleId)
.withKeyId(keyId)
.sign(algorithm);
}
private PrivateKey parsePrivateKey(String privateKeyPem) throws AppStoreAPIException {
try {
Reader reader = new StringReader(privateKeyPem);
PEMParser pemParser = new PEMParser(reader);
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
Object object = pemParser.readObject();
if (object instanceof PrivateKeyInfo) {
return converter.getPrivateKey((PrivateKeyInfo) object);
} else {
return converter.getPrivateKey((org.bouncycastle.openssl.PKCS8EncryptedKeyPair) object);
}
} catch (Exception e) {
throw new AppStoreAPIException("Failed to parse private key", e);
}
}
private String buildQueryString(Map<String, String> params) {
List<String> pairs = new ArrayList<>();
for (Map.Entry<String, String> param : params.entrySet()) {
pairs.add(param.getKey() + "=" + param.getValue());
}
return String.join("&", pairs);
}
}
class AppStoreAPIException extends Exception {
public AppStoreAPIException(String message) {
super(message);
}
public AppStoreAPIException(String message, Throwable cause) {
super(message, cause);
}
}
// Response models for App Store Server API
class TransactionHistoryResponse {
private String appAppleId;
private String bundleId;
private String environment;
private boolean hasMore;
private String revision;
private List<JWSTransaction> transactions;
// Getters and Setters
public String getAppAppleId() { return appAppleId; }
public void setAppAppleId(String appAppleId) { this.appAppleId = appAppleId; }
public String getBundleId() { return bundleId; }
public void setBundleId(String bundleId) { this.bundleId = bundleId; }
public String getEnvironment() { return environment; }
public void setEnvironment(String environment) { this.environment = environment; }
public boolean isHasMore() { return hasMore; }
public void setHasMore(boolean hasMore) { this.hasMore = hasMore; }
public String getRevision() { return revision; }
public void setRevision(String revision) { this.revision = revision; }
public List<JWSTransaction> getTransactions() { return transactions; }
public void setTransactions(List<JWSTransaction> transactions) { this.transactions = transactions; }
}
class SubscriptionStatusResponse {
private String environment;
private String appAppleId;
private String bundleId;
private List<SubscriptionGroupIdentifierItem> data;
// Getters and Setters
public String getEnvironment() { return environment; }
public void setEnvironment(String environment) { this.environment = environment; }
public String getAppAppleId() { return appAppleId; }
public void setAppAppleId(String appAppleId) { this.appAppleId = appAppleId; }
public String getBundleId() { return bundleId; }
public void setBundleId(String bundleId) { this.bundleId = bundleId; }
public List<SubscriptionGroupIdentifierItem> getData() { return data; }
public void setData(List<SubscriptionGroupIdentifierItem> data) { this.data = data; }
}
class LookupOrderIdResponse {
private int status;
private List<String> transactionIds;
// Getters and Setters
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public List<String> getTransactionIds() { return transactionIds; }
public void setTransactionIds(List<String> transactionIds) { this.transactionIds = transactionIds; }
}
class ExtendRenewalDateResponse {
private String effectiveDate;
private String originalTransactionId;
private String webOrderLineItemId;
private boolean success;
// Getters and Setters
public String getEffectiveDate() { return effectiveDate; }
public void setEffectiveDate(String effectiveDate) { this.effectiveDate = effectiveDate; }
public String getOriginalTransactionId() { return originalTransactionId; }
public void setOriginalTransactionId(String originalTransactionId) { this.originalTransactionId = originalTransactionId; }
public String getWebOrderLineItemId() { return webOrderLineItemId; }
public void setWebOrderLineItemId(String webOrderLineItemId) { this.webOrderLineItemId = webOrderLineItemId; }
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
}
class JWSTransaction {
private String transactionId;
private String originalTransactionId;
private String webOrderLineItemId;
private String bundleId;
private String productId;
private String subscriptionGroupIdentifier;
private LocalDateTime purchaseDate;
private LocalDateTime originalPurchaseDate;
private LocalDateTime expiresDate;
private int quantity;
private String type;
private String inAppOwnershipType;
private String signedDate;
// Getters and Setters
public String getTransactionId() { return transactionId; }
public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
public String getOriginalTransactionId() { return originalTransactionId; }
public void setOriginalTransactionId(String originalTransactionId) { this.originalTransactionId = originalTransactionId; }
public String getWebOrderLineItemId() { return webOrderLineItemId; }
public void setWebOrderLineItemId(String webOrderLineItemId) { this.webOrderLineItemId = webOrderLineItemId; }
public String getBundleId() { return bundleId; }
public void setBundleId(String bundleId) { this.bundleId = bundleId; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getSubscriptionGroupIdentifier() { return subscriptionGroupIdentifier; }
public void setSubscriptionGroupIdentifier(String subscriptionGroupIdentifier) { this.subscriptionGroupIdentifier = subscriptionGroupIdentifier; }
public LocalDateTime getPurchaseDate() { return purchaseDate; }
public void setPurchaseDate(LocalDateTime purchaseDate) { this.purchaseDate = purchaseDate; }
public LocalDateTime getOriginalPurchaseDate() { return originalPurchaseDate; }
public void setOriginalPurchaseDate(LocalDateTime originalPurchaseDate) { this.originalPurchaseDate = originalPurchaseDate; }
public LocalDateTime getExpiresDate() { return expiresDate; }
public void setExpiresDate(LocalDateTime expiresDate) { this.expiresDate = expiresDate; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getInAppOwnershipType() { return inAppOwnershipType; }
public void setInAppOwnershipType(String inAppOwnershipType) { this.inAppOwnershipType = inAppOwnershipType; }
public String getSignedDate() { return signedDate; }
public void setSignedDate(String signedDate) { this.signedDate = signedDate; }
}
class SubscriptionGroupIdentifierItem {
private String subscriptionGroupIdentifier;
private List<LastTransactionsItem> lastTransactions;
// Getters and Setters
public String getSubscriptionGroupIdentifier() { return subscriptionGroupIdentifier; }
public void setSubscriptionGroupIdentifier(String subscriptionGroupIdentifier) { this.subscriptionGroupIdentifier = subscriptionGroupIdentifier; }
public List<LastTransactionsItem> getLastTransactions() { return lastTransactions; }
public void setLastTransactions(List<LastTransactionsItem> lastTransactions) { this.lastTransactions = lastTransactions; }
}
class LastTransactionsItem {
private String originalTransactionId;
private String status;
private String signedRenewalInfo;
private String signedTransactionInfo;
// Getters and Setters
public String getOriginalTransactionId() { return originalTransactionId; }
public void setOriginalTransactionId(String originalTransactionId) { this.originalTransactionId = originalTransactionId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getSignedRenewalInfo() { return signedRenewalInfo; }
public void setSignedRenewalInfo(String signedRenewalInfo) { this.signedRenewalInfo = signedRenewalInfo; }
public String getSignedTransactionInfo() { return signedTransactionInfo; }
public void setSignedTransactionInfo(String signedTransactionInfo) { this.signedTransactionInfo = signedTransactionInfo; }
}
class ExtendRenewalDateRequest {
private int extendByDays;
private String extendReasonCode;
private String requestIdentifier;
// Getters and Setters
public int getExtendByDays() { return extendByDays; }
public void setExtendByDays(int extendByDays) { this.extendByDays = extendByDays; }
public String getExtendReasonCode() { return extendReasonCode; }
public void setExtendReasonCode(String extendReasonCode) { this.extendReasonCode = extendReasonCode; }
public String getRequestIdentifier() { return requestIdentifier; }
public void setRequestIdentifier(String requestIdentifier) { this.requestIdentifier = requestIdentifier; }
}

5. IAP Service Implementation

package com.example.iap.service;
import com.example.iap.api.AppStoreServerAPI;
import com.example.iap.model.*;
import com.example.iap.validation.ReceiptValidator;
import com.example.iap.validation.ReceiptValidationResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.util.*;
public class IAPService {
private static final Logger log = LoggerFactory.getLogger(IAPService.class);
private final ReceiptValidator receiptValidator;
private final AppStoreServerAPI appStoreAPI;
private final SubscriptionManager subscriptionManager;
private final ProductCatalog productCatalog;
public IAPService(ReceiptValidator receiptValidator, AppStoreServerAPI appStoreAPI,
SubscriptionManager subscriptionManager, ProductCatalog productCatalog) {
this.receiptValidator = receiptValidator;
this.appStoreAPI = appStoreAPI;
this.subscriptionManager = subscriptionManager;
this.productCatalog = productCatalog;
}
public PurchaseResult processPurchase(String userId, String receiptData, String productId) {
try {
// Validate receipt
ReceiptValidationResult validationResult = receiptValidator.validateReceipt(receiptData);
if (!validationResult.isValid()) {
return PurchaseResult.failed("Invalid receipt: " + validationResult.getErrorMessage());
}
// Find the specific purchase
Optional<ProcessedPurchase> purchaseOpt = validationResult.getPurchases().stream()
.filter(p -> p.getProductId().equals(productId))
.findFirst();
if (purchaseOpt.isEmpty()) {
return PurchaseResult.failed("Product not found in receipt");
}
ProcessedPurchase purchase = purchaseOpt.get();
// Check if this is a duplicate transaction
if (subscriptionManager.isDuplicateTransaction(userId, purchase.getTransactionId())) {
return PurchaseResult.failed("Duplicate transaction");
}
// Process based on product type
IAPProduct product = productCatalog.getProduct(productId);
if (product == null) {
return PurchaseResult.failed("Unknown product");
}
switch (product.getType()) {
case CONSUMABLE:
return processConsumablePurchase(userId, purchase, product);
case NON_CONSUMABLE:
return processNonConsumablePurchase(userId, purchase, product);
case AUTO_RENEWABLE_SUBSCRIPTION:
return processSubscriptionPurchase(userId, purchase, product, validationResult);
case NON_RENEWABLE_SUBSCRIPTION:
return processNonRenewableSubscription(userId, purchase, product);
default:
return PurchaseResult.failed("Unsupported product type");
}
} catch (Exception e) {
log.error("Failed to process purchase", e);
return PurchaseResult.failed("Processing error: " + e.getMessage());
}
}
public SubscriptionStatusResult checkSubscriptionStatus(String userId, String originalTransactionId) {
try {
UserSubscription subscription = subscriptionManager.getUserSubscription(userId, originalTransactionId);
if (subscription == null) {
return SubscriptionStatusResult.notFound();
}
// Check if we need to refresh from App Store
if (shouldRefreshSubscription(subscription)) {
subscription = refreshSubscriptionFromAppStore(subscription);
}
SubscriptionStatusResult result = new SubscriptionStatusResult();
result.setSubscription(subscription);
result.setActive(subscription.isActive());
result.setExpiresSoon(isExpiringSoon(subscription));
return result;
} catch (Exception e) {
log.error("Failed to check subscription status", e);
return SubscriptionStatusResult.error(e.getMessage());
}
}
public void handleServerToServerNotification(ServerToServerNotification notification) {
try {
log.info("Processing server-to-server notification: {}", notification.getNotificationType());
switch (notification.getNotificationType()) {
case INITIAL_BUY:
case DID_RENEW:
handleSubscriptionRenewal(notification);
break;
case DID_FAIL_TO_RENEW:
handleRenewalFailure(notification);
break;
case DID_CHANGE_RENEWAL_PREF:
handleRenewalPreferenceChange(notification);
break;
case DID_CHANGE_RENEWAL_STATUS:
handleRenewalStatusChange(notification);
break;
case REFUND:
handleRefund(notification);
break;
case CANCEL:
handleCancellation(notification);
break;
default:
log.warn("Unhandled notification type: {}", notification.getNotificationType());
}
} catch (Exception e) {
log.error("Failed to process server-to-server notification", e);
}
}
private PurchaseResult processConsumablePurchase(String userId, ProcessedPurchase purchase, IAPProduct product) {
// Grant consumable product to user
boolean granted = grantConsumableToUser(userId, product, purchase.getQuantity());
if (granted) {
subscriptionManager.recordPurchase(userId, purchase, product.getType());
return PurchaseResult.success(product, purchase);
} else {
return PurchaseResult.failed("Failed to grant consumable product");
}
}
private PurchaseResult processNonConsumablePurchase(String userId, ProcessedPurchase purchase, IAPProduct product) {
// Check if user already owns this non-consumable
if (subscriptionManager.userOwnsProduct(userId, product.getProductId())) {
return PurchaseResult.failed("User already owns this product");
}
// Grant non-consumable product
boolean granted = grantNonConsumableToUser(userId, product);
if (granted) {
subscriptionManager.recordPurchase(userId, purchase, product.getType());
return PurchaseResult.success(product, purchase);
} else {
return PurchaseResult.failed("Failed to grant non-consumable product");
}
}
private PurchaseResult processSubscriptionPurchase(String userId, ProcessedPurchase purchase, 
IAPProduct product, ReceiptValidationResult validationResult) {
// Determine subscription status
SubscriptionStatus status = receiptValidator.determineSubscriptionStatus(
purchase, validationResult.getPendingRenewalInfo());
// Create or update subscription
UserSubscription subscription = createOrUpdateSubscription(
userId, purchase, product, status, validationResult.getEnvironment());
if (subscription != null) {
return PurchaseResult.success(product, purchase, subscription);
} else {
return PurchaseResult.failed("Failed to create subscription");
}
}
private PurchaseResult processNonRenewableSubscription(String userId, ProcessedPurchase purchase, IAPProduct product) {
// For non-renewable subscriptions, we need to track expiration manually
UserSubscription subscription = createNonRenewableSubscription(userId, purchase, product);
if (subscription != null) {
return PurchaseResult.success(product, purchase, subscription);
} else {
return PurchaseResult.failed("Failed to create non-renewable subscription");
}
}
private UserSubscription createOrUpdateSubscription(String userId, ProcessedPurchase purchase, 
IAPProduct product, SubscriptionStatus status, 
Environment environment) {
UserSubscription subscription = new UserSubscription();
subscription.setUserId(userId);
subscription.setOriginalTransactionId(purchase.getOriginalTransactionId());
subscription.setProductId(product.getProductId());
subscription.setProductType(product.getType());
subscription.setStatus(status);
subscription.setPurchaseDate(purchase.getPurchaseDate());
subscription.setExpiresDate(purchase.getExpiresDate());
subscription.setCancellationDate(purchase.getCancellationDate());
subscription.setAutoRenewEnabled(isAutoRenewEnabled(purchase, status));
subscription.setEnvironment(environment.getValue());
subscription.setTrialPeriod(purchase.isTrialPeriod());
subscription.setInIntroOfferPeriod(purchase.isIntroOfferPeriod());
subscription.setCreatedAt(LocalDateTime.now());
subscription.setUpdatedAt(LocalDateTime.now());
return subscriptionManager.saveSubscription(subscription);
}
private boolean isAutoRenewEnabled(ProcessedPurchase purchase, SubscriptionStatus status) {
// Logic to determine if auto-renew is enabled based on status and purchase data
return status == SubscriptionStatus.ACTIVE || status == SubscriptionStatus.GRACE_PERIOD;
}
private boolean shouldRefreshSubscription(UserSubscription subscription) {
// Refresh if subscription is about to expire or if we haven't refreshed recently
LocalDateTime now = LocalDateTime.now();
return subscription.getExpiresDate() != null && 
subscription.getExpiresDate().minusHours(24).isBefore(now) &&
subscription.getUpdatedAt().isBefore(now.minusHours(6));
}
private UserSubscription refreshSubscriptionFromAppStore(UserSubscription subscription) {
try {
// Use App Store Server API to get latest status
SubscriptionStatusResponse statusResponse = appStoreAPI.getSubscriptionStatus(
subscription.getOriginalTransactionId(), 
"Sandbox".equals(subscription.getEnvironment())
);
// Update subscription with latest information
return subscriptionManager.updateFromAppStoreResponse(subscription, statusResponse);
} catch (Exception e) {
log.error("Failed to refresh subscription from App Store", e);
return subscription;
}
}
private boolean isExpiringSoon(UserSubscription subscription) {
if (subscription.getExpiresDate() == null) return false;
LocalDateTime now = LocalDateTime.now();
return subscription.getExpiresDate().isAfter(now) && 
subscription.getExpiresDate().isBefore(now.plusDays(3));
}
private void handleSubscriptionRenewal(ServerToServerNotification notification) {
// Update subscription with new expiration date
subscriptionManager.handleRenewal(
notification.getOriginalTransactionId(),
notification.getExpiresDate()
);
}
private void handleRenewalFailure(ServerToServerNotification notification) {
// Mark subscription as in billing retry or expired
subscriptionManager.handleRenewalFailure(
notification.getOriginalTransactionId(),
notification.getExpirationIntent()
);
}
// Other notification handlers...
private boolean grantConsumableToUser(String userId, IAPProduct product, int quantity) {
// Implementation to grant consumable products to user
// This would typically update user's balance in database
return true;
}
private boolean grantNonConsumableToUser(String userId, IAPProduct product) {
// Implementation to grant non-consumable products to user
// This would typically add product to user's owned products
return true;
}
private UserSubscription createNonRenewableSubscription(String userId, ProcessedPurchase purchase, IAPProduct product) {
// Implementation for non-renewable subscriptions
// These have fixed durations and don't auto-renew
return null;
}
}
class PurchaseResult {
private boolean success;
private String errorMessage;
private IAPProduct product;
private ProcessedPurchase purchase;
private UserSubscription subscription;
// Static factory methods
public static PurchaseResult success(IAPProduct product, ProcessedPurchase purchase) {
PurchaseResult result = new PurchaseResult();
result.success = true;
result.product = product;
result.purchase = purchase;
return result;
}
public static PurchaseResult success(IAPProduct product, ProcessedPurchase purchase, UserSubscription subscription) {
PurchaseResult result = new PurchaseResult();
result.success = true;
result.product = product;
result.purchase = purchase;
result.subscription = subscription;
return result;
}
public static PurchaseResult failed(String errorMessage) {
PurchaseResult result = new PurchaseResult();
result.success = false;
result.errorMessage = errorMessage;
return result;
}
// Getters
public boolean isSuccess() { return success; }
public String getErrorMessage() { return errorMessage; }
public IAPProduct getProduct() { return product; }
public ProcessedPurchase getPurchase() { return purchase; }
public UserSubscription getSubscription() { return subscription; }
}
class SubscriptionStatusResult {
private UserSubscription subscription;
private boolean active;
private boolean expiresSoon;
private String error;
// Static factory methods
public static SubscriptionStatusResult notFound() {
SubscriptionStatusResult result = new SubscriptionStatusResult();
result.error = "Subscription not found";
return result;
}
public static SubscriptionStatusResult error(String error) {
SubscriptionStatusResult result = new SubscriptionStatusResult();
result.error = error;
return result;
}
// Getters and Setters
public UserSubscription getSubscription() { return subscription; }
public void setSubscription(UserSubscription subscription) { this.subscription = subscription; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
public boolean isExpiresSoon() { return expiresSoon; }
public void setExpiresSoon(boolean expiresSoon) { this.expiresSoon = expiresSoon; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
public boolean hasError() { return error != null; }
}
class ServerToServerNotification {
private String notificationType;
private String subtype;
private String notificationUUID;
private String version;
private NotificationData data;
// Getters and Setters
public String getNotificationType() { return notificationType; }
public void setNotificationType(String notificationType) { this.notificationType = notificationType; }
public String getSubtype() { return subtype; }
public void setSubtype(String subtype) { this.subtype = subtype; }
public String getNotificationUUID() { return notificationUUID; }
public void setNotificationUUID(String notificationUUID) { this.notificationUUID = notificationUUID; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public NotificationData getData() { return data; }
public void setData(NotificationData data) { this.data = data; }
// Helper methods
public String getBundleId() { return data != null ? data.getBundleId() : null; }
public String getOriginalTransactionId() { return data != null ? data.getOriginalTransactionId() : null; }
public String getProductId() { return data != null ? data.getProductId() : null; }
public LocalDateTime getExpiresDate() { return data != null ? data.getExpiresDate() : null; }
public String getExpirationIntent() { return data != null ? data.getExpirationIntent() : null; }
}
class NotificationData {
private String appAppleId;
private String bundleId;
private String bundleVersion;
private String environment;
private RenewalInfo renewalInfo;
private TransactionInfo transactionInfo;
// Getters and Setters
public String getAppAppleId() { return appAppleId; }
public void setAppAppleId(String appAppleId) { this.appAppleId = appAppleId; }
public String getBundleId() { return bundleId; }
public void setBundleId(String bundleId) { this.bundleId = bundleId; }
public String getBundleVersion() { return bundleVersion; }
public void setBundleVersion(String bundleVersion) { this.bundleVersion = bundleVersion; }
public String getEnvironment() { return environment; }
public void setEnvironment(String environment) { this.environment = environment; }
public RenewalInfo getRenewalInfo() { return renewalInfo; }
public void setRenewalInfo(RenewalInfo renewalInfo) { this.renewalInfo = renewalInfo; }
public TransactionInfo getTransactionInfo() { return transactionInfo; }
public void setTransactionInfo(TransactionInfo transactionInfo) { this.transactionInfo = transactionInfo; }
// Helper methods
public String getOriginalTransactionId() {
return transactionInfo != null ? transactionInfo.getOriginalTransactionId() : null;
}
public String getProductId() {
return transactionInfo != null ? transactionInfo.getProductId() : null;
}
public LocalDateTime getExpiresDate() {
return transactionInfo != null ? transactionInfo.getExpiresDate() : null;
}
public String getExpirationIntent() {
return renewalInfo != null ? renewalInfo.getExpirationIntent() : null;
}
}
class RenewalInfo {
private String expirationIntent;
private String autoRenewProductId;
private String autoRenewStatus;
private String offerIdentifier;
private String offerType;
// Getters and Setters
public String getExpirationIntent() { return expirationIntent; }
public void setExpirationIntent(String expirationIntent) { this.expirationIntent = expirationIntent; }
public String getAutoRenewProductId() { return autoRenewProductId; }
public void setAutoRenewProductId(String autoRenewProductId) { this.autoRenewProductId = autoRenewProductId; }
public String getAutoRenewStatus() { return autoRenewStatus; }
public void setAutoRenewStatus(String autoRenewStatus) { this.autoRenewStatus = autoRenewStatus; }
public String getOfferIdentifier() { return offerIdentifier; }
public void setOfferIdentifier(String offerIdentifier) { this.offerIdentifier = offerIdentifier; }
public String getOfferType() { return offerType; }
public void setOfferType(String offerType) { this.offerType = offerType; }
}
class TransactionInfo {
private String transactionId;
private String originalTransactionId;
private String webOrderLineItemId;
private String bundleId;
private String productId;
private String subscriptionGroupIdentifier;
private LocalDateTime purchaseDate;
private LocalDateTime originalPurchaseDate;
private LocalDateTime expiresDate;
private int quantity;
private String type;
private String inAppOwnershipType;
private String signedDate;
// Getters and Setters
public String getTransactionId() { return transactionId; }
public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
public String getOriginalTransactionId() { return originalTransactionId; }
public void setOriginalTransactionId(String originalTransactionId) { this.originalTransactionId = originalTransactionId; }
public String getWebOrderLineItemId() { return webOrderLineItemId; }
public void setWebOrderLineItemId(String webOrderLineItemId) { this.webOrderLineItemId = webOrderLineItemId; }
public String getBundleId() { return bundleId; }
public void setBundleId(String bundleId) { this.bundleId = bundleId; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public String getSubscriptionGroupIdentifier() { return subscriptionGroupIdentifier; }
public void setSubscriptionGroupIdentifier(String subscriptionGroupIdentifier) { this.subscriptionGroupIdentifier = subscriptionGroupIdentifier; }
public LocalDateTime getPurchaseDate() { return purchaseDate; }
public void setPurchaseDate(LocalDateTime purchaseDate) { this.purchaseDate = purchaseDate; }
public LocalDateTime getOriginalPurchaseDate() { return originalPurchaseDate; }
public void setOriginalPurchaseDate(LocalDateTime originalPurchaseDate) { this.originalPurchaseDate = originalPurchaseDate; }
public LocalDateTime getExpiresDate() { return expiresDate; }
public void setExpiresDate(LocalDateTime expiresDate) { this.expiresDate = expiresDate; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getInAppOwnershipType() { return inAppOwnershipType; }
public void setInAppOwnershipType(String inAppOwnershipType) { this.inAppOwnershipType = inAppOwnershipType; }
public String getSignedDate() { return signedDate; }
public void setSignedDate(String signedDate) { this.signedDate = signedDate; }
}

6. Subscription Manager

package com.example.iap.service;
import com.example.iap.api.SubscriptionStatusResponse;
import com.example.iap.model.*;
import com.example.iap.validation.ProcessedPurchase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
public class SubscriptionManager {
private static final Logger log = LoggerFactory.getLogger(SubscriptionManager.class);
// In production, this would be a database repository
// For this example, we'll use in-memory storage
// private final SubscriptionRepository subscriptionRepository;
public UserSubscription getUserSubscription(String userId, String originalTransactionId) {
// Implementation to get subscription from database
// return subscriptionRepository.findByUserIdAndOriginalTransactionId(userId, originalTransactionId);
return null; // Placeholder
}
public List<UserSubscription> getUserSubscriptions(String userId) {
// Implementation to get all user subscriptions
// return subscriptionRepository.findByUserId(userId);
return List.of(); // Placeholder
}
public boolean isDuplicateTransaction(String userId, String transactionId) {
// Check if this transaction has already been processed
// return subscriptionRepository.existsByUserIdAndTransactionId(userId, transactionId);
return false; // Placeholder
}
public boolean userOwnsProduct(String userId, String productId) {
// Check if user already owns this non-consumable product
// return subscriptionRepository.existsByUserIdAndProductIdAndProductType(
//     userId, productId, ProductType.NON_CONSUMABLE);
return false; // Placeholder
}
public UserSubscription saveSubscription(UserSubscription subscription) {
// Save or update subscription in database
// return subscriptionRepository.save(subscription);
return subscription; // Placeholder
}
public void recordPurchase(String userId, ProcessedPurchase purchase, ProductType productType) {
// Record the purchase transaction
// PurchaseRecord record = new PurchaseRecord(userId, purchase, productType);
// purchaseRecordRepository.save(record);
}
public UserSubscription updateFromAppStoreResponse(UserSubscription subscription, 
SubscriptionStatusResponse response) {
// Update subscription with latest information from App Store
if (response.getData() != null && !response.getData().isEmpty()) {
// Process the latest transaction information
// This would update expiration dates, status, etc.
subscription.setUpdatedAt(LocalDateTime.now());
return saveSubscription(subscription);
}
return subscription;
}
public void handleRenewal(String originalTransactionId, LocalDateTime newExpiresDate) {
// Update subscription with new expiration date
Optional<UserSubscription> subscriptionOpt = findSubscriptionByOriginalTransactionId(originalTransactionId);
if (subscriptionOpt.isPresent()) {
UserSubscription subscription = subscriptionOpt.get();
subscription.setExpiresDate(newExpiresDate);
subscription.setStatus(SubscriptionStatus.ACTIVE);
subscription.setUpdatedAt(LocalDateTime.now());
saveSubscription(subscription);
log.info("Subscription renewed: {}", originalTransactionId);
}
}
public void handleRenewalFailure(String originalTransactionId, String expirationIntent) {
// Handle failed renewal
Optional<UserSubscription> subscriptionOpt = findSubscriptionByOriginalTransactionId(originalTransactionId);
if (subscriptionOpt.isPresent()) {
UserSubscription subscription = subscriptionOpt.get();
// Determine status based on expiration intent
SubscriptionStatus status = determineStatusFromExpirationIntent(expirationIntent);
subscription.setStatus(status);
subscription.setUpdatedAt(LocalDateTime.now());
saveSubscription(subscription);
log.warn("Subscription renewal failed: {}, intent: {}", originalTransactionId, expirationIntent);
}
}
private Optional<UserSubscription> findSubscriptionByOriginalTransactionId(String originalTransactionId) {
// Implementation to find subscription by original transaction ID
// return subscriptionRepository.findByOriginalTransactionId(originalTransactionId);
return Optional.empty(); // Placeholder
}
private SubscriptionStatus determineStatusFromExpirationIntent(String expirationIntent) {
if (expirationIntent == null) return SubscriptionStatus.EXPIRED;
switch (expirationIntent) {
case "1": return SubscriptionStatus.CANCELLED; // Voluntary cancellation
case "2": return SubscriptionStatus.EXPIRED; // Billing error
case "3": return SubscriptionStatus.EXPIRED; // Price increase rejection
case "4": return SubscriptionStatus.EXPIRED; // Product unavailable
case "5": return SubscriptionStatus.EXPIRED; // Unknown
default: return SubscriptionStatus.EXPIRED;
}
}
}
class ProductCatalog {
public IAPProduct getProduct(String productId) {
// Implementation to get product from catalog/database
// In production, this would fetch from a database or cache
return null; // Placeholder
}
public List<IAPProduct> getProductsByType(ProductType type) {
// Implementation to get products by type
return List.of(); // Placeholder
}
public boolean validateProduct(String productId, String bundleId) {
// Validate that product exists and belongs to the correct app
IAPProduct product = getProduct(productId);
return product != null && product.isActive();
}
}

7. Example Usage

package com.example.iap.demo;
import com.example.iap.api.AppStoreServerAPI;
import com.example.iap.model.*;
import com.example.iap.service.IAPService;
import com.example.iap.service.ProductCatalog;
import com.example.iap.service.SubscriptionManager;
import com.example.iap.validation.ReceiptValidator;
public class IAPDemo {
public static void main(String[] args) {
try {
// Initialize services
ReceiptValidator receiptValidator = new ReceiptValidator(
"your-shared-secret", 
true // use sandbox for testing
);
AppStoreServerAPI appStoreAPI = new AppStoreServerAPI(
"your-issuer-id",
"com.yourapp.bundleid", 
"your-key-id",
"-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY\n-----END PRIVATE KEY-----"
);
SubscriptionManager subscriptionManager = new SubscriptionManager();
ProductCatalog productCatalog = new ProductCatalog();
IAPService iapService = new IAPService(
receiptValidator, 
appStoreAPI, 
subscriptionManager, 
productCatalog
);
// Example 1: Process a purchase
String userId = "user-123";
String receiptData = "base64-encoded-receipt-data";
String productId = "com.yourapp.premium_subscription";
PurchaseResult result = iapService.processPurchase(userId, receiptData, productId);
if (result.isSuccess()) {
System.out.println("Purchase successful!");
System.out.println("Product: " + result.getProduct().getTitle());
System.out.println("Transaction: " + result.getPurchase().getTransactionId());
if (result.getSubscription() != null) {
System.out.println("Subscription active until: " + result.getSubscription().getExpiresDate());
}
} else {
System.out.println("Purchase failed: " + result.getErrorMessage());
}
// Example 2: Check subscription status
String originalTransactionId = "1000000000000000";
SubscriptionStatusResult statusResult = iapService.checkSubscriptionStatus(userId, originalTransactionId);
if (!statusResult.hasError()) {
System.out.println("Subscription active: " + statusResult.isActive());
System.out.println("Expires soon: " + statusResult.isExpiresSoon());
System.out.println("Status: " + statusResult.getSubscription().getStatus());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// Example web controller for handling IAP
/*
@RestController
@RequestMapping("/api/iap")
public class IAPController {
@Autowired
private IAPService iapService;
@PostMapping("/purchase")
public ResponseEntity<PurchaseResult> processPurchase(
@RequestBody PurchaseRequest request) {
PurchaseResult result = iapService.processPurchase(
request.getUserId(),
request.getReceiptData(),
request.getProductId()
);
return ResponseEntity.ok(result);
}
@GetMapping("/subscription/{userId}/{transactionId}")
public ResponseEntity<SubscriptionStatusResult> getSubscriptionStatus(
@PathVariable String userId,
@PathVariable String transactionId) {
SubscriptionStatusResult result = iapService.checkSubscriptionStatus(userId, transactionId);
return ResponseEntity.ok(result);
}
@PostMapping("/server-notification")
public ResponseEntity<Void> handleServerNotification(
@RequestBody ServerToServerNotification notification) {
iapService.handleServerToServerNotification(notification);
return ResponseEntity.ok().build();
}
}
class PurchaseRequest {
private String userId;
private String receiptData;
private String productId;
// Getters and Setters
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getReceiptData() { return receiptData; }
public void setReceiptData(String receiptData) { this.receiptData = receiptData; }
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
}
*/

Key Features

  1. Receipt Validation: Complete Apple receipt validation with sandbox/production support
  2. App Store Server API: Integration with Apple's newer server-to-server API
  3. Subscription Management: Comprehensive subscription status tracking and renewal handling
  4. Product Catalog: Flexible product management system
  5. Server-to-Server Notifications: Real-time handling of subscription events
  6. Error Handling: Robust error handling and status code management
  7. Security: JWT generation for API authentication and receipt security
  8. Multi-Environment: Support for both sandbox and production environments

This implementation provides a complete Apple In-App Purchase solution in Java for server-side receipt validation, subscription management, and IAP processing.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper