Skip to main content

storekit/
transaction.rs

1use core::ffi::c_void;
2use core::ptr;
3use std::ptr::NonNull;
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7
8use crate::advanced_commerce::{
9    TransactionAdvancedCommerceInfo, TransactionAdvancedCommerceInfoPayload,
10};
11use crate::app_store::AppStoreEnvironment;
12use crate::error::{StoreKitError, VerificationFailure};
13use crate::ffi;
14use crate::private::{
15    cstring_from_str, decode_base64, duration_to_timeout_ms, error_from_status, json_cstring,
16    parse_json_ptr, parse_optional_json_ptr,
17};
18use crate::product::ProductType;
19use crate::refund::{Refund, RefundRequestStatus};
20use crate::storefront::{Storefront, StorefrontPayload};
21use crate::subscription::{SubscriptionPeriod, SubscriptionPeriodPayload};
22pub use crate::verification_result::VerificationResult;
23
24use crate::verification_result::VerificationResultPayload;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum TransactionReason {
28    Purchase,
29    Renewal,
30    Unknown(String),
31}
32
33impl TransactionReason {
34    pub fn as_str(&self) -> &str {
35        match self {
36            Self::Purchase => "purchase",
37            Self::Renewal => "renewal",
38            Self::Unknown(value) => value.as_str(),
39        }
40    }
41
42    fn from_raw(raw: String) -> Self {
43        match raw.as_str() {
44            "purchase" => Self::Purchase,
45            "renewal" => Self::Renewal,
46            _ => Self::Unknown(raw),
47        }
48    }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum RevocationReason {
53    DeveloperIssue,
54    Other,
55    Unknown(String),
56}
57
58impl RevocationReason {
59    pub fn as_str(&self) -> &str {
60        match self {
61            Self::DeveloperIssue => "developerIssue",
62            Self::Other => "other",
63            Self::Unknown(value) => value.as_str(),
64        }
65    }
66
67    fn from_raw(raw: String) -> Self {
68        match raw.as_str() {
69            "developerIssue" => Self::DeveloperIssue,
70            "other" => Self::Other,
71            _ => Self::Unknown(raw),
72        }
73    }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum OfferType {
78    Introductory,
79    Promotional,
80    Code,
81    WinBack,
82    Unknown(String),
83}
84
85impl OfferType {
86    pub fn as_str(&self) -> &str {
87        match self {
88            Self::Introductory => "introductory",
89            Self::Promotional => "promotional",
90            Self::Code => "code",
91            Self::WinBack => "winBack",
92            Self::Unknown(value) => value.as_str(),
93        }
94    }
95
96    fn from_raw(raw: String) -> Self {
97        match raw.as_str() {
98            "introductory" => Self::Introductory,
99            "promotional" => Self::Promotional,
100            "code" => Self::Code,
101            "winBack" => Self::WinBack,
102            _ => Self::Unknown(raw),
103        }
104    }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum OfferPaymentMode {
109    FreeTrial,
110    PayAsYouGo,
111    PayUpFront,
112    OneTime,
113    Unknown(String),
114}
115
116impl OfferPaymentMode {
117    pub fn as_str(&self) -> &str {
118        match self {
119            Self::FreeTrial => "freeTrial",
120            Self::PayAsYouGo => "payAsYouGo",
121            Self::PayUpFront => "payUpFront",
122            Self::OneTime => "oneTime",
123            Self::Unknown(value) => value.as_str(),
124        }
125    }
126
127    fn from_raw(raw: String) -> Self {
128        match raw.as_str() {
129            "freeTrial" => Self::FreeTrial,
130            "payAsYouGo" => Self::PayAsYouGo,
131            "payUpFront" => Self::PayUpFront,
132            "oneTime" => Self::OneTime,
133            _ => Self::Unknown(raw),
134        }
135    }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct TransactionOffer {
140    pub id: Option<String>,
141    pub offer_type: OfferType,
142    pub payment_mode: Option<OfferPaymentMode>,
143    pub period: Option<SubscriptionPeriod>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub enum OwnershipType {
148    Purchased,
149    FamilyShared,
150    Unknown(String),
151}
152
153impl OwnershipType {
154    fn from_raw(raw: String) -> Self {
155        match raw.as_str() {
156            "purchased" => Self::Purchased,
157            "familyShared" => Self::FamilyShared,
158            _ => Self::Unknown(raw),
159        }
160    }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct TransactionData {
165    pub id: u64,
166    pub original_id: u64,
167    pub web_order_line_item_id: Option<String>,
168    pub product_id: String,
169    pub subscription_group_id: Option<String>,
170    pub app_bundle_id: String,
171    pub purchase_date: String,
172    pub original_purchase_date: String,
173    pub expiration_date: Option<String>,
174    pub purchased_quantity: u64,
175    pub is_upgraded: bool,
176    pub ownership_type: OwnershipType,
177    pub signed_date: String,
178    pub jws_representation: String,
179    pub verification_failure: Option<VerificationFailure>,
180    pub revocation_date: Option<String>,
181    pub revocation_reason: Option<RevocationReason>,
182    pub product_type: Option<ProductType>,
183    pub app_account_token: Option<String>,
184    pub environment: Option<AppStoreEnvironment>,
185    pub reason: Option<TransactionReason>,
186    pub storefront: Option<Storefront>,
187    pub price: Option<String>,
188    pub currency_code: Option<String>,
189    pub app_transaction_id: Option<String>,
190    pub offer: Option<TransactionOffer>,
191    pub json_representation: Vec<u8>,
192}
193
194#[derive(Debug)]
195pub struct Transaction {
196    handle: Option<NonNull<c_void>>,
197    data: TransactionData,
198    advanced_commerce_info: Option<TransactionAdvancedCommerceInfo>,
199}
200
201impl Clone for Transaction {
202    fn clone(&self) -> Self {
203        let handle = self.handle.map(|handle| {
204            // SAFETY: handle is a valid, non-null StoreKit transaction pointer maintained
205            // by this Transaction.  sk_transaction_retain increments the retain count
206            // and returns the same pointer (never null for a live transaction).
207            let retained = unsafe { ffi::sk_transaction_retain(handle.as_ptr()) };
208            NonNull::new(retained).expect("StoreKit transaction retain returned null")
209        });
210        Self {
211            handle,
212            data: self.data.clone(),
213            advanced_commerce_info: self.advanced_commerce_info.clone(),
214        }
215    }
216}
217
218impl Drop for Transaction {
219    fn drop(&mut self) {
220        if let Some(handle) = self.handle {
221            // SAFETY: handle is a valid, non-null StoreKit transaction pointer that
222            // this Transaction uniquely owns (or co-owns with a matching retain from
223            // Clone).  Drop is the unique release point per ownership token.
224            unsafe { ffi::sk_transaction_release(handle.as_ptr()) };
225        }
226    }
227}
228
229impl Transaction {
230    pub fn current_entitlements() -> Result<TransactionStream, StoreKitError> {
231        TransactionStream::new(&TransactionStreamConfig::current_entitlements())
232    }
233
234    pub fn all() -> Result<TransactionStream, StoreKitError> {
235        TransactionStream::new(&TransactionStreamConfig::all())
236    }
237
238    pub fn updates() -> Result<TransactionStream, StoreKitError> {
239        TransactionStream::new(&TransactionStreamConfig::updates())
240    }
241
242    pub fn unfinished() -> Result<TransactionStream, StoreKitError> {
243        TransactionStream::new(&TransactionStreamConfig::unfinished())
244    }
245
246    pub fn all_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
247        TransactionStream::new(&TransactionStreamConfig::all_for(product_id))
248    }
249
250    pub fn current_entitlements_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
251        TransactionStream::new(&TransactionStreamConfig::current_entitlements_for(
252            product_id,
253        ))
254    }
255
256    pub fn latest_for(product_id: &str) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
257        let product_id = cstring_from_str(product_id, "product id")?;
258        let mut transaction_handle = ptr::null_mut();
259        let mut result_json = ptr::null_mut();
260        let mut error_message = ptr::null_mut();
261        let status = unsafe {
262            ffi::sk_transaction_latest_for(
263                product_id.as_ptr(),
264                &mut transaction_handle,
265                &mut result_json,
266                &mut error_message,
267            )
268        };
269        if status != ffi::status::OK {
270            return Err(unsafe { error_from_status(status, error_message) });
271        }
272
273        let payload = unsafe {
274            parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
275                result_json,
276                "latest transaction",
277            )
278        }?;
279        payload
280            .map(|payload| {
281                payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
282            })
283            .transpose()
284    }
285
286    pub fn current_entitlement_for(
287        product_id: &str,
288    ) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
289        let product_id = cstring_from_str(product_id, "product id")?;
290        let mut transaction_handle = ptr::null_mut();
291        let mut result_json = ptr::null_mut();
292        let mut error_message = ptr::null_mut();
293        let status = unsafe {
294            ffi::sk_transaction_current_entitlement_for(
295                product_id.as_ptr(),
296                &mut transaction_handle,
297                &mut result_json,
298                &mut error_message,
299            )
300        };
301        if status != ffi::status::OK {
302            return Err(unsafe { error_from_status(status, error_message) });
303        }
304
305        let payload = unsafe {
306            parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
307                result_json,
308                "current entitlement transaction",
309            )
310        }?;
311        payload
312            .map(|payload| {
313                payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
314            })
315            .transpose()
316    }
317
318    pub const fn data(&self) -> &TransactionData {
319        &self.data
320    }
321
322    pub const fn advanced_commerce_info(&self) -> Option<&TransactionAdvancedCommerceInfo> {
323        self.advanced_commerce_info.as_ref()
324    }
325
326    pub const fn has_live_handle(&self) -> bool {
327        self.handle.is_some()
328    }
329
330    pub fn verify(&self) -> Result<(), StoreKitError> {
331        self.handle.map_or_else(
332            || {
333                self.data
334                    .verification_failure
335                    .clone()
336                    .map_or(Ok(()), |failure| Err(StoreKitError::Verification(failure)))
337            },
338            |handle| {
339                let mut error_message = ptr::null_mut();
340                let status =
341                    unsafe { ffi::sk_transaction_verify(handle.as_ptr(), &mut error_message) };
342                if status == ffi::status::OK {
343                    Ok(())
344                } else {
345                    Err(unsafe { error_from_status(status, error_message) })
346                }
347            },
348        )
349    }
350
351    pub fn finish(&self) -> Result<(), StoreKitError> {
352        self.handle.map_or_else(
353            || {
354                Err(StoreKitError::NotSupported(
355                    "transaction snapshots cannot be finished because they do not carry a live StoreKit handle"
356                        .to_owned(),
357                ))
358            },
359            |handle| {
360                let mut error_message = ptr::null_mut();
361                let status = unsafe { ffi::sk_transaction_finish(handle.as_ptr(), &mut error_message) };
362                if status == ffi::status::OK {
363                    Ok(())
364                } else {
365                    Err(unsafe { error_from_status(status, error_message) })
366                }
367            },
368        )
369    }
370
371    pub fn begin_refund_request(&self) -> Result<RefundRequestStatus, StoreKitError> {
372        Refund::begin_for_transaction_id(self.data.id)
373    }
374
375    pub(crate) fn from_raw_parts(
376        handle: *mut c_void,
377        payload: TransactionPayload,
378    ) -> Result<Self, StoreKitError> {
379        let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
380        Ok(Self {
381            handle: NonNull::new(handle),
382            data,
383            advanced_commerce_info,
384        })
385    }
386
387    pub(crate) fn from_snapshot_payload(
388        payload: TransactionPayload,
389    ) -> Result<Self, StoreKitError> {
390        let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
391        Ok(Self {
392            handle: None,
393            data,
394            advanced_commerce_info,
395        })
396    }
397}
398
399#[derive(Debug)]
400pub struct TransactionStream {
401    handle: NonNull<c_void>,
402    finished: bool,
403}
404
405impl Drop for TransactionStream {
406    fn drop(&mut self) {
407        unsafe { ffi::sk_transaction_stream_release(self.handle.as_ptr()) };
408    }
409}
410
411impl TransactionStream {
412    fn new(config: &TransactionStreamConfig) -> Result<Self, StoreKitError> {
413        let config_json = json_cstring(config, "transaction stream config")?;
414        let mut error_message = ptr::null_mut();
415        let handle =
416            unsafe { ffi::sk_transaction_stream_create(config_json.as_ptr(), &mut error_message) };
417        let handle = NonNull::new(handle)
418            .ok_or_else(|| unsafe { error_from_status(ffi::status::UNKNOWN, error_message) })?;
419        Ok(Self {
420            handle,
421            finished: false,
422        })
423    }
424
425    pub const fn is_finished(&self) -> bool {
426        self.finished
427    }
428
429    #[allow(clippy::should_implement_trait)]
430    pub fn next(&mut self) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
431        self.next_timeout(Duration::from_secs(30))
432    }
433
434    pub fn next_timeout(
435        &mut self,
436        timeout: Duration,
437    ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
438        let mut transaction_handle = ptr::null_mut();
439        let mut verification_json = ptr::null_mut();
440        let mut error_message = ptr::null_mut();
441        let status = unsafe {
442            ffi::sk_transaction_stream_next(
443                self.handle.as_ptr(),
444                duration_to_timeout_ms(timeout),
445                &mut transaction_handle,
446                &mut verification_json,
447                &mut error_message,
448            )
449        };
450
451        match status {
452            ffi::status::OK => {
453                let payload = unsafe {
454                    parse_json_ptr::<VerificationResultPayload<TransactionPayload>>(
455                        verification_json,
456                        "transaction verification result",
457                    )
458                };
459                match payload {
460                    Ok(payload) => payload
461                        .into_result(|payload| {
462                            Transaction::from_raw_parts(transaction_handle, payload)
463                        })
464                        .map(Some),
465                    Err(error) => {
466                        if !transaction_handle.is_null() {
467                            unsafe { ffi::sk_transaction_release(transaction_handle) };
468                        }
469                        Err(error)
470                    }
471                }
472            }
473            ffi::status::END_OF_STREAM => {
474                self.finished = true;
475                Ok(None)
476            }
477            ffi::status::TIMED_OUT => Ok(None),
478            _ => Err(unsafe { error_from_status(status, error_message) }),
479        }
480    }
481}
482
483#[derive(Debug, Serialize)]
484struct TransactionStreamConfig {
485    kind: &'static str,
486    #[serde(rename = "productID", skip_serializing_if = "Option::is_none")]
487    product_id: Option<String>,
488}
489
490impl TransactionStreamConfig {
491    const fn all() -> Self {
492        Self {
493            kind: "all",
494            product_id: None,
495        }
496    }
497
498    const fn current_entitlements() -> Self {
499        Self {
500            kind: "currentEntitlements",
501            product_id: None,
502        }
503    }
504
505    const fn updates() -> Self {
506        Self {
507            kind: "updates",
508            product_id: None,
509        }
510    }
511
512    const fn unfinished() -> Self {
513        Self {
514            kind: "unfinished",
515            product_id: None,
516        }
517    }
518
519    fn all_for(product_id: &str) -> Self {
520        Self {
521            kind: "allFor",
522            product_id: Some(product_id.to_owned()),
523        }
524    }
525
526    fn current_entitlements_for(product_id: &str) -> Self {
527        Self {
528            kind: "currentEntitlementsFor",
529            product_id: Some(product_id.to_owned()),
530        }
531    }
532}
533
534#[derive(Debug, Deserialize)]
535pub(crate) struct TransactionOfferPayload {
536    id: Option<String>,
537    #[serde(rename = "type")]
538    offer_type: String,
539    #[serde(rename = "paymentMode")]
540    payment_mode: Option<String>,
541    period: Option<SubscriptionPeriodPayload>,
542}
543
544impl TransactionOfferPayload {
545    pub(crate) fn into_transaction_offer(self) -> TransactionOffer {
546        TransactionOffer {
547            id: self.id,
548            offer_type: OfferType::from_raw(self.offer_type),
549            payment_mode: self.payment_mode.map(OfferPaymentMode::from_raw),
550            period: self
551                .period
552                .map(SubscriptionPeriodPayload::into_subscription_period),
553        }
554    }
555}
556
557#[derive(Debug, Deserialize)]
558pub(crate) struct TransactionPayload {
559    id: u64,
560    #[serde(rename = "originalID")]
561    original_id: u64,
562    #[serde(rename = "webOrderLineItemID")]
563    web_order_line_item_id: Option<String>,
564    #[serde(rename = "productID")]
565    product_id: String,
566    #[serde(rename = "subscriptionGroupID")]
567    subscription_group_id: Option<String>,
568    #[serde(rename = "appBundleID")]
569    app_bundle_id: String,
570    #[serde(rename = "purchaseDate")]
571    purchase_date: String,
572    #[serde(rename = "originalPurchaseDate")]
573    original_purchase_date: String,
574    #[serde(rename = "expirationDate")]
575    expiration_date: Option<String>,
576    #[serde(rename = "purchasedQuantity")]
577    purchased_quantity: u64,
578    #[serde(rename = "isUpgraded")]
579    is_upgraded: bool,
580    #[serde(rename = "ownershipType")]
581    ownership_type: String,
582    #[serde(rename = "signedDate")]
583    signed_date: String,
584    #[serde(rename = "jwsRepresentation")]
585    jws_representation: String,
586    #[serde(rename = "verificationError")]
587    verification_error: Option<crate::error::VerificationErrorPayload>,
588    #[serde(rename = "revocationDate")]
589    revocation_date: Option<String>,
590    #[serde(rename = "revocationReason")]
591    revocation_reason: Option<String>,
592    #[serde(rename = "productType")]
593    product_type: Option<String>,
594    #[serde(rename = "appAccountToken")]
595    app_account_token: Option<String>,
596    environment: Option<String>,
597    reason: Option<String>,
598    storefront: Option<StorefrontPayload>,
599    price: Option<String>,
600    #[serde(rename = "currencyCode")]
601    currency_code: Option<String>,
602    #[serde(rename = "appTransactionID")]
603    app_transaction_id: Option<String>,
604    offer: Option<TransactionOfferPayload>,
605    #[serde(rename = "advancedCommerceInfo")]
606    advanced_commerce_info: Option<TransactionAdvancedCommerceInfoPayload>,
607    #[serde(rename = "jsonRepresentationBase64")]
608    json_representation_base64: String,
609}
610
611impl TransactionPayload {
612    fn into_transaction_parts(
613        self,
614    ) -> Result<(TransactionData, Option<TransactionAdvancedCommerceInfo>), StoreKitError> {
615        let advanced_commerce_info = self
616            .advanced_commerce_info
617            .map(TransactionAdvancedCommerceInfoPayload::into_transaction_advanced_commerce_info);
618        Ok((TransactionData {
619            id: self.id,
620            original_id: self.original_id,
621            web_order_line_item_id: self.web_order_line_item_id,
622            product_id: self.product_id,
623            subscription_group_id: self.subscription_group_id,
624            app_bundle_id: self.app_bundle_id,
625            purchase_date: self.purchase_date,
626            original_purchase_date: self.original_purchase_date,
627            expiration_date: self.expiration_date,
628            purchased_quantity: self.purchased_quantity,
629            is_upgraded: self.is_upgraded,
630            ownership_type: OwnershipType::from_raw(self.ownership_type),
631            signed_date: self.signed_date,
632            jws_representation: self.jws_representation,
633            verification_failure: self
634                .verification_error
635                .map(crate::error::VerificationFailure::from_payload),
636            revocation_date: self.revocation_date,
637            revocation_reason: self.revocation_reason.map(RevocationReason::from_raw),
638            product_type: self.product_type.map(ProductType::from_raw),
639            app_account_token: self.app_account_token,
640            environment: self.environment.map(AppStoreEnvironment::from_raw),
641            reason: self.reason.map(TransactionReason::from_raw),
642            storefront: self.storefront.map(StorefrontPayload::into_storefront),
643            price: self.price,
644            currency_code: self.currency_code,
645            app_transaction_id: self.app_transaction_id,
646            offer: self
647                .offer
648                .map(TransactionOfferPayload::into_transaction_offer),
649            json_representation: decode_base64(
650                &self.json_representation_base64,
651                "transaction JSON representation",
652            )?,
653        }, advanced_commerce_info))
654    }
655}