iapiab 0.1.0

iapiab verifies the purchase receipt via AppStore or GooglePlayStore.
Documentation
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Serialize, Deserialize)]
pub enum Environment {
    Sandbox,
    Production,
}

// https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
// The IAPRequest type has the request parameter
#[derive(Debug, Serialize, Deserialize)]
pub struct IAPRequest {
    #[serde(rename = "receipt-data")]
    pub receipt_data: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub password: Option<String>,
    #[serde(rename = "exclude-old-transactions")]
    pub exclude_old_transactions: bool,
}

// The InApp type has the receipt attributes
// https://developer.apple.com/documentation/appstorereceipts/responsebody/receipt/in_app
#[derive(Debug, Serialize, Deserialize)]
pub struct InApp {
    pub cancellation_date: Option<String>,
    pub cancellation_date_ms: Option<String>,
    pub cancellation_date_pst: Option<String>,
    pub cancellation_reason: Option<String>,

    pub expires_date: Option<String>,
    pub expires_date_ms: Option<String>,
    pub expires_date_pst: Option<String>,

    pub is_in_intro_offer_period: Option<String>,
    pub is_trial_period: String,

    pub original_purchase_date: String,
    pub original_purchase_date_ms: String,
    pub original_purchase_date_pst: String,

    pub original_transaction_id: String,
    pub product_id: String,
    pub promotional_offer_id: Option<String>,

    pub purchase_date: String,
    pub purchase_date_ms: String,
    pub purchase_date_pst: String,

    pub quantity: String,
    pub transaction_id: String,
    pub web_order_line_item_id: Option<String>,
}

// The InApp type has the receipt attributes
// https://developer.apple.com/documentation/appstorereceipts/responsebody/latest_receipt_info
#[derive(Debug, Serialize, Deserialize)]
pub struct LatestReceiptInfo {
    pub subscription_group_identifier: Option<String>,
    pub in_app_ownership_type: Option<String>,
    pub is_upgraded: Option<String>,

    #[serde(flatten)]
    pub in_app: InApp,
}

// The Receipt type has whole data of receipt
// https://developer.apple.com/documentation/appstorereceipts/responsebody/receipt
#[derive(Debug, Serialize, Deserialize)]
pub struct Receipt {
    pub adam_id: i64,
    pub app_item_id: i64,
    pub application_version: String,
    pub bundle_id: String,
    pub download_id: i64,

    pub expiration_date: Option<String>,
    pub expiration_date_ms: Option<String>,
    pub expiration_date_pst: Option<String>,

    pub in_app: Vec<InApp>,

    pub original_application_version: String,
    pub original_purchase_date: String,
    pub original_purchase_date_ms: String,
    pub original_purchase_date_pst: String,

    pub preorder_date: Option<String>,
    pub preorder_date_ms: Option<String>,
    pub preorder_date_pst: Option<String>,

    pub receipt_creation_date: String,
    pub receipt_creation_date_ms: String,
    pub receipt_creation_date_pst: String,

    pub receipt_type: String,

    pub request_date: String,
    pub request_date_ms: String,
    pub request_date_pst: String,

    pub version_external_identifier: i64,
}

// A pending renewal may refer to a renewal that is scheduled in the future or a renewal that failed in the past for some reason.
// https://developer.apple.com/documentation/appstorereceipts/responsebody/pending_renewal_info
#[derive(Debug, Serialize, Deserialize)]
pub struct PendingRenewalInfo {
    pub auto_renew_product_id: String,
    pub auto_renew_status: String,
    pub expiration_intent: Option<String>,

    pub grace_period_expires_date: Option<String>,
    pub grace_period_expires_date_ms: Option<String>,
    pub grace_period_expires_date_pst: Option<String>,

    pub is_in_billing_retry_period: String,
    pub offer_code_ref_name: Option<String>,
    pub original_transacyytion_id: String,
    pub price_consent_status: String,
    pub product_id: String,
    pub promotional_offer_id: Option<String>,
}

// The IAPResponse type has the response properties
// https://developer.apple.com/documentation/appstorereceipts/responsebody
#[derive(Debug, Serialize, Deserialize)]
pub struct IAPResponse {
    pub status: i32,
    pub environment: Option<Environment>,
    pub receipt: Option<Receipt>,
    pub is_retryable: Option<bool>,

    // only for subscription
    pub latest_receipt_info: Option<Vec<LatestReceiptInfo>>,
    pub latest_receipt: Option<String>,
    pub pending_renewal_info: Option<Vec<PendingRenewalInfo>>,
}

#[derive(Error, Debug, PartialEq)]
pub enum AppStoreVerifyError {
    #[error("The App Store cloud not read the JSON object you provided.")]
    InvalidJson,
    #[error("The data in the receipt-data property was malformed or missing.")]
    InvalidReceiptData,
    #[error("The receipt could not be authenticated.")]
    ReceiptUnauthenticated,
    #[error(
        "The shared secret you provided does not match the shared secret on file for your account."
    )]
    InvalidSharedSecret,
    #[error("The receipt server is not currently available.")]
    ServerUnavailable,
    #[error("This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.")]
    ReceiptIsForTest,
    #[error("This receipt is from the production environment, but it was sent to the production environment for verification. Send it to the production environment instead.")]
    ReceiptIsForProduction,
    #[error(
        "The receipt could not be authorized. Treat this the same as if a purchase was never made."
    )]
    ReceiptUnauthorized,
    #[error("Internal data access error.")]
    InternalDataAccessError,
    #[error("An unknown error occurred")]
    Unknown,
}

impl IAPResponse {
    pub fn error(&self) -> Option<AppStoreVerifyError> {
        match self.status {
            0 => None,
            21000 => Some(AppStoreVerifyError::InvalidJson),
            21002 => Some(AppStoreVerifyError::InvalidReceiptData),
            21003 => Some(AppStoreVerifyError::ReceiptUnauthenticated),
            21004 => Some(AppStoreVerifyError::InvalidSharedSecret),
            21005 => Some(AppStoreVerifyError::ServerUnavailable),
            21007 => Some(AppStoreVerifyError::ReceiptIsForTest),
            21008 => Some(AppStoreVerifyError::ReceiptIsForProduction),
            21009 | 21100..=21199 => Some(AppStoreVerifyError::InternalDataAccessError),
            21010 => Some(AppStoreVerifyError::ReceiptUnauthorized),
            _ => Some(AppStoreVerifyError::Unknown),
        }
    }
}