storekit-rs 0.2.0

Safe Rust bindings for Apple's StoreKit framework — in-app purchases and transaction streams on macOS
Documentation
use core::ffi::c_char;
use std::fmt;

use serde::Deserialize;

use crate::ffi;
use crate::private::take_string;

#[derive(Debug, Clone)]
pub enum StoreKitError {
    InvalidArgument(String),
    TimedOut(String),
    NotSupported(String),
    Framework(StoreKitFrameworkError),
    Verification(VerificationFailure),
    Unknown(String),
}

impl fmt::Display for StoreKitError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidArgument(message)
            | Self::TimedOut(message)
            | Self::NotSupported(message)
            | Self::Unknown(message) => formatter.write_str(message),
            Self::Framework(error) => write!(
                formatter,
                "{} (domain={}, code={})",
                error.localized_description, error.domain, error.code
            ),
            Self::Verification(error) => write!(
                formatter,
                "{} ({})",
                error.localized_description,
                error.code.as_str()
            ),
        }
    }
}

impl std::error::Error for StoreKitError {}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoreKitFrameworkError {
    pub domain: String,
    pub code: i64,
    pub localized_description: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerificationFailure {
    pub code: VerificationErrorCode,
    pub localized_description: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerificationErrorCode {
    RevokedCertificate,
    InvalidCertificateChain,
    InvalidDeviceVerification,
    InvalidEncoding,
    InvalidSignature,
    MissingRequiredProperties,
    Unknown(String),
}

impl VerificationErrorCode {
    pub fn as_str(&self) -> &str {
        match self {
            Self::RevokedCertificate => "revokedCertificate",
            Self::InvalidCertificateChain => "invalidCertificateChain",
            Self::InvalidDeviceVerification => "invalidDeviceVerification",
            Self::InvalidEncoding => "invalidEncoding",
            Self::InvalidSignature => "invalidSignature",
            Self::MissingRequiredProperties => "missingRequiredProperties",
            Self::Unknown(value) => value.as_str(),
        }
    }

    pub(crate) fn from_raw(raw: String) -> Self {
        match raw.as_str() {
            "revokedCertificate" => Self::RevokedCertificate,
            "invalidCertificateChain" => Self::InvalidCertificateChain,
            "invalidDeviceVerification" => Self::InvalidDeviceVerification,
            "invalidEncoding" => Self::InvalidEncoding,
            "invalidSignature" => Self::InvalidSignature,
            "missingRequiredProperties" => Self::MissingRequiredProperties,
            _ => Self::Unknown(raw),
        }
    }
}

#[derive(Debug, Deserialize)]
pub(crate) struct VerificationErrorPayload {
    #[allow(dead_code)]
    kind: String,
    code: String,
    #[serde(rename = "localizedDescription")]
    localized_description: String,
}

impl VerificationFailure {
    pub(crate) fn from_payload(payload: VerificationErrorPayload) -> Self {
        Self {
            code: VerificationErrorCode::from_raw(payload.code),
            localized_description: payload.localized_description,
        }
    }
}

#[derive(Debug, Deserialize)]
struct FrameworkErrorPayload {
    #[allow(dead_code)]
    kind: String,
    domain: String,
    code: i64,
    #[serde(rename = "localizedDescription")]
    localized_description: String,
}

pub(crate) unsafe fn from_swift(status: i32, err_msg: *mut c_char) -> StoreKitError {
    let message = take_string(err_msg);
    match status {
        ffi::status::INVALID_ARGUMENT => StoreKitError::InvalidArgument(
            message.unwrap_or_else(|| "StoreKit reported an invalid argument".to_owned()),
        ),
        ffi::status::TIMED_OUT => StoreKitError::TimedOut(
            message.unwrap_or_else(|| "StoreKit operation timed out".to_owned()),
        ),
        ffi::status::NOT_SUPPORTED => StoreKitError::NotSupported(
            message.unwrap_or_else(|| "StoreKit operation is not supported".to_owned()),
        ),
        ffi::status::FRAMEWORK_ERROR => parse_framework_error(message),
        ffi::status::VERIFICATION_ERROR => parse_verification_error(message),
        _ => StoreKitError::Unknown(
            message
                .unwrap_or_else(|| format!("StoreKit bridge returned unexpected status {status}")),
        ),
    }
}

fn parse_framework_error(message: Option<String>) -> StoreKitError {
    message.map_or_else(
        || {
            StoreKitError::Framework(StoreKitFrameworkError {
                domain: "StoreKit".to_owned(),
                code: i64::from(ffi::status::FRAMEWORK_ERROR),
                localized_description: "StoreKit framework error".to_owned(),
            })
        },
        |json| {
            serde_json::from_str::<FrameworkErrorPayload>(&json).map_or_else(
                |_| {
                    StoreKitError::Framework(StoreKitFrameworkError {
                        domain: "StoreKit".to_owned(),
                        code: i64::from(ffi::status::FRAMEWORK_ERROR),
                        localized_description: json,
                    })
                },
                |payload| {
                    StoreKitError::Framework(StoreKitFrameworkError {
                        domain: payload.domain,
                        code: payload.code,
                        localized_description: payload.localized_description,
                    })
                },
            )
        },
    )
}

fn parse_verification_error(message: Option<String>) -> StoreKitError {
    message.map_or_else(
        || {
            StoreKitError::Verification(VerificationFailure {
                code: VerificationErrorCode::Unknown("unknown".to_owned()),
                localized_description: "StoreKit verification failed".to_owned(),
            })
        },
        |json| {
            serde_json::from_str::<VerificationErrorPayload>(&json).map_or_else(
                |_| {
                    StoreKitError::Verification(VerificationFailure {
                        code: VerificationErrorCode::Unknown("unknown".to_owned()),
                        localized_description: json,
                    })
                },
                |payload| StoreKitError::Verification(VerificationFailure::from_payload(payload)),
            )
        },
    )
}