Skip to main content

storekit/
receipt_validator.rs

1use serde::Deserialize;
2use serde_json::Value;
3
4use crate::app_transaction::AppTransaction;
5use crate::error::StoreKitError;
6use crate::ffi;
7use crate::private::{
8    decode_base64, decode_base64_urlsafe, error_from_status, parse_optional_json_ptr,
9};
10use crate::verification_result::VerificationResult;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13/// Carries the app receipt used with `StoreKit` receipt helpers.
14pub struct AppReceipt {
15    /// Filesystem path returned by `StoreKit`.
16    pub path: String,
17    /// Binary data returned by `StoreKit`.
18    pub data: Vec<u8>,
19}
20
21#[derive(Debug, Clone, Copy, Default)]
22/// Helpers for `StoreKit` receipts and signed payloads.
23pub struct ReceiptValidator;
24
25impl ReceiptValidator {
26    /// Fetches the current app receipt from `StoreKit`.
27    pub fn current_receipt() -> Result<Option<AppReceipt>, StoreKitError> {
28        let mut receipt_json = core::ptr::null_mut();
29        let mut error_message = core::ptr::null_mut();
30        let status = unsafe { ffi::sk_receipt_json(&mut receipt_json, &mut error_message) };
31        if status != ffi::status::OK {
32            return Err(unsafe { error_from_status(status, error_message) });
33        }
34
35        unsafe { parse_optional_json_ptr::<AppReceiptPayload>(receipt_json, "receipt") }
36            .and_then(|payload| payload.map(AppReceiptPayload::into_receipt).transpose())
37    }
38
39    /// Fetches `StoreKit.AppTransaction.shared`.
40    pub fn shared_app_transaction() -> Result<VerificationResult<AppTransaction>, StoreKitError> {
41        AppTransaction::shared()
42    }
43
44    /// Fetches `StoreKit.AppTransaction.refresh()`.
45    pub fn refresh_app_transaction() -> Result<VerificationResult<AppTransaction>, StoreKitError> {
46        AppTransaction::refresh()
47    }
48
49    /// Decodes a `StoreKit` JWS payload without verifying its signature.
50    pub fn extract_unverified_payload(jws: &str) -> Result<Value, StoreKitError> {
51        let mut segments = jws.split('.');
52        let _header = segments.next().ok_or_else(|| {
53            StoreKitError::InvalidArgument("JWS is missing a header segment".to_owned())
54        })?;
55        let payload = segments.next().ok_or_else(|| {
56            StoreKitError::InvalidArgument("JWS is missing a payload segment".to_owned())
57        })?;
58        let _signature = segments.next().ok_or_else(|| {
59            StoreKitError::InvalidArgument("JWS is missing a signature segment".to_owned())
60        })?;
61        if segments.next().is_some() {
62            return Err(StoreKitError::InvalidArgument(
63                "JWS contains too many segments".to_owned(),
64            ));
65        }
66        let payload_bytes = decode_base64_urlsafe(payload, "JWS payload")?;
67        serde_json::from_slice::<Value>(&payload_bytes).map_err(|error| {
68            StoreKitError::InvalidArgument(format!("failed to decode JWS payload JSON: {error}"))
69        })
70    }
71}
72
73#[derive(Debug, Deserialize)]
74struct AppReceiptPayload {
75    path: String,
76    #[serde(rename = "dataBase64")]
77    data_base64: String,
78}
79
80impl AppReceiptPayload {
81    fn into_receipt(self) -> Result<AppReceipt, StoreKitError> {
82        Ok(AppReceipt {
83            path: self.path,
84            data: decode_base64(&self.data_base64, "receipt data")?,
85        })
86    }
87}