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            let retained = unsafe { ffi::sk_transaction_retain(handle.as_ptr()) };
205            NonNull::new(retained).expect("StoreKit transaction retain returned null")
206        });
207        Self {
208            handle,
209            data: self.data.clone(),
210            advanced_commerce_info: self.advanced_commerce_info.clone(),
211        }
212    }
213}
214
215impl Drop for Transaction {
216    fn drop(&mut self) {
217        if let Some(handle) = self.handle {
218            unsafe { ffi::sk_transaction_release(handle.as_ptr()) };
219        }
220    }
221}
222
223impl Transaction {
224    pub fn current_entitlements() -> Result<TransactionStream, StoreKitError> {
225        TransactionStream::new(&TransactionStreamConfig::current_entitlements())
226    }
227
228    pub fn all() -> Result<TransactionStream, StoreKitError> {
229        TransactionStream::new(&TransactionStreamConfig::all())
230    }
231
232    pub fn updates() -> Result<TransactionStream, StoreKitError> {
233        TransactionStream::new(&TransactionStreamConfig::updates())
234    }
235
236    pub fn unfinished() -> Result<TransactionStream, StoreKitError> {
237        TransactionStream::new(&TransactionStreamConfig::unfinished())
238    }
239
240    pub fn all_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
241        TransactionStream::new(&TransactionStreamConfig::all_for(product_id))
242    }
243
244    pub fn current_entitlements_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
245        TransactionStream::new(&TransactionStreamConfig::current_entitlements_for(
246            product_id,
247        ))
248    }
249
250    pub fn latest_for(product_id: &str) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
251        let product_id = cstring_from_str(product_id, "product id")?;
252        let mut transaction_handle = ptr::null_mut();
253        let mut result_json = ptr::null_mut();
254        let mut error_message = ptr::null_mut();
255        let status = unsafe {
256            ffi::sk_transaction_latest_for(
257                product_id.as_ptr(),
258                &mut transaction_handle,
259                &mut result_json,
260                &mut error_message,
261            )
262        };
263        if status != ffi::status::OK {
264            return Err(unsafe { error_from_status(status, error_message) });
265        }
266
267        let payload = unsafe {
268            parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
269                result_json,
270                "latest transaction",
271            )
272        }?;
273        payload
274            .map(|payload| {
275                payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
276            })
277            .transpose()
278    }
279
280    pub fn current_entitlement_for(
281        product_id: &str,
282    ) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
283        let product_id = cstring_from_str(product_id, "product id")?;
284        let mut transaction_handle = ptr::null_mut();
285        let mut result_json = ptr::null_mut();
286        let mut error_message = ptr::null_mut();
287        let status = unsafe {
288            ffi::sk_transaction_current_entitlement_for(
289                product_id.as_ptr(),
290                &mut transaction_handle,
291                &mut result_json,
292                &mut error_message,
293            )
294        };
295        if status != ffi::status::OK {
296            return Err(unsafe { error_from_status(status, error_message) });
297        }
298
299        let payload = unsafe {
300            parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
301                result_json,
302                "current entitlement transaction",
303            )
304        }?;
305        payload
306            .map(|payload| {
307                payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
308            })
309            .transpose()
310    }
311
312    pub const fn data(&self) -> &TransactionData {
313        &self.data
314    }
315
316    pub const fn advanced_commerce_info(&self) -> Option<&TransactionAdvancedCommerceInfo> {
317        self.advanced_commerce_info.as_ref()
318    }
319
320    pub const fn has_live_handle(&self) -> bool {
321        self.handle.is_some()
322    }
323
324    pub fn verify(&self) -> Result<(), StoreKitError> {
325        self.handle.map_or_else(
326            || {
327                self.data
328                    .verification_failure
329                    .clone()
330                    .map_or(Ok(()), |failure| Err(StoreKitError::Verification(failure)))
331            },
332            |handle| {
333                let mut error_message = ptr::null_mut();
334                let status =
335                    unsafe { ffi::sk_transaction_verify(handle.as_ptr(), &mut error_message) };
336                if status == ffi::status::OK {
337                    Ok(())
338                } else {
339                    Err(unsafe { error_from_status(status, error_message) })
340                }
341            },
342        )
343    }
344
345    pub fn finish(&self) -> Result<(), StoreKitError> {
346        self.handle.map_or_else(
347            || {
348                Err(StoreKitError::NotSupported(
349                    "transaction snapshots cannot be finished because they do not carry a live StoreKit handle"
350                        .to_owned(),
351                ))
352            },
353            |handle| {
354                let mut error_message = ptr::null_mut();
355                let status = unsafe { ffi::sk_transaction_finish(handle.as_ptr(), &mut error_message) };
356                if status == ffi::status::OK {
357                    Ok(())
358                } else {
359                    Err(unsafe { error_from_status(status, error_message) })
360                }
361            },
362        )
363    }
364
365    pub fn begin_refund_request(&self) -> Result<RefundRequestStatus, StoreKitError> {
366        Refund::begin_for_transaction_id(self.data.id)
367    }
368
369    pub(crate) fn from_raw_parts(
370        handle: *mut c_void,
371        payload: TransactionPayload,
372    ) -> Result<Self, StoreKitError> {
373        let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
374        Ok(Self {
375            handle: NonNull::new(handle),
376            data,
377            advanced_commerce_info,
378        })
379    }
380
381    pub(crate) fn from_snapshot_payload(
382        payload: TransactionPayload,
383    ) -> Result<Self, StoreKitError> {
384        let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
385        Ok(Self {
386            handle: None,
387            data,
388            advanced_commerce_info,
389        })
390    }
391}
392
393#[derive(Debug)]
394pub struct TransactionStream {
395    handle: NonNull<c_void>,
396    finished: bool,
397}
398
399impl Drop for TransactionStream {
400    fn drop(&mut self) {
401        unsafe { ffi::sk_transaction_stream_release(self.handle.as_ptr()) };
402    }
403}
404
405impl TransactionStream {
406    fn new(config: &TransactionStreamConfig) -> Result<Self, StoreKitError> {
407        let config_json = json_cstring(config, "transaction stream config")?;
408        let mut error_message = ptr::null_mut();
409        let handle =
410            unsafe { ffi::sk_transaction_stream_create(config_json.as_ptr(), &mut error_message) };
411        let handle = NonNull::new(handle)
412            .ok_or_else(|| unsafe { error_from_status(ffi::status::UNKNOWN, error_message) })?;
413        Ok(Self {
414            handle,
415            finished: false,
416        })
417    }
418
419    pub const fn is_finished(&self) -> bool {
420        self.finished
421    }
422
423    #[allow(clippy::should_implement_trait)]
424    pub fn next(&mut self) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
425        self.next_timeout(Duration::from_secs(30))
426    }
427
428    pub fn next_timeout(
429        &mut self,
430        timeout: Duration,
431    ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
432        let mut transaction_handle = ptr::null_mut();
433        let mut verification_json = ptr::null_mut();
434        let mut error_message = ptr::null_mut();
435        let status = unsafe {
436            ffi::sk_transaction_stream_next(
437                self.handle.as_ptr(),
438                duration_to_timeout_ms(timeout),
439                &mut transaction_handle,
440                &mut verification_json,
441                &mut error_message,
442            )
443        };
444
445        match status {
446            ffi::status::OK => {
447                let payload = unsafe {
448                    parse_json_ptr::<VerificationResultPayload<TransactionPayload>>(
449                        verification_json,
450                        "transaction verification result",
451                    )
452                };
453                match payload {
454                    Ok(payload) => payload
455                        .into_result(|payload| {
456                            Transaction::from_raw_parts(transaction_handle, payload)
457                        })
458                        .map(Some),
459                    Err(error) => {
460                        if !transaction_handle.is_null() {
461                            unsafe { ffi::sk_transaction_release(transaction_handle) };
462                        }
463                        Err(error)
464                    }
465                }
466            }
467            ffi::status::END_OF_STREAM => {
468                self.finished = true;
469                Ok(None)
470            }
471            ffi::status::TIMED_OUT => Ok(None),
472            _ => Err(unsafe { error_from_status(status, error_message) }),
473        }
474    }
475}
476
477#[derive(Debug, Serialize)]
478struct TransactionStreamConfig {
479    kind: &'static str,
480    #[serde(rename = "productID", skip_serializing_if = "Option::is_none")]
481    product_id: Option<String>,
482}
483
484impl TransactionStreamConfig {
485    const fn all() -> Self {
486        Self {
487            kind: "all",
488            product_id: None,
489        }
490    }
491
492    const fn current_entitlements() -> Self {
493        Self {
494            kind: "currentEntitlements",
495            product_id: None,
496        }
497    }
498
499    const fn updates() -> Self {
500        Self {
501            kind: "updates",
502            product_id: None,
503        }
504    }
505
506    const fn unfinished() -> Self {
507        Self {
508            kind: "unfinished",
509            product_id: None,
510        }
511    }
512
513    fn all_for(product_id: &str) -> Self {
514        Self {
515            kind: "allFor",
516            product_id: Some(product_id.to_owned()),
517        }
518    }
519
520    fn current_entitlements_for(product_id: &str) -> Self {
521        Self {
522            kind: "currentEntitlementsFor",
523            product_id: Some(product_id.to_owned()),
524        }
525    }
526}
527
528#[derive(Debug, Deserialize)]
529pub(crate) struct TransactionOfferPayload {
530    id: Option<String>,
531    #[serde(rename = "type")]
532    offer_type: String,
533    #[serde(rename = "paymentMode")]
534    payment_mode: Option<String>,
535    period: Option<SubscriptionPeriodPayload>,
536}
537
538impl TransactionOfferPayload {
539    pub(crate) fn into_transaction_offer(self) -> TransactionOffer {
540        TransactionOffer {
541            id: self.id,
542            offer_type: OfferType::from_raw(self.offer_type),
543            payment_mode: self.payment_mode.map(OfferPaymentMode::from_raw),
544            period: self
545                .period
546                .map(SubscriptionPeriodPayload::into_subscription_period),
547        }
548    }
549}
550
551#[derive(Debug, Deserialize)]
552pub(crate) struct TransactionPayload {
553    id: u64,
554    #[serde(rename = "originalID")]
555    original_id: u64,
556    #[serde(rename = "webOrderLineItemID")]
557    web_order_line_item_id: Option<String>,
558    #[serde(rename = "productID")]
559    product_id: String,
560    #[serde(rename = "subscriptionGroupID")]
561    subscription_group_id: Option<String>,
562    #[serde(rename = "appBundleID")]
563    app_bundle_id: String,
564    #[serde(rename = "purchaseDate")]
565    purchase_date: String,
566    #[serde(rename = "originalPurchaseDate")]
567    original_purchase_date: String,
568    #[serde(rename = "expirationDate")]
569    expiration_date: Option<String>,
570    #[serde(rename = "purchasedQuantity")]
571    purchased_quantity: u64,
572    #[serde(rename = "isUpgraded")]
573    is_upgraded: bool,
574    #[serde(rename = "ownershipType")]
575    ownership_type: String,
576    #[serde(rename = "signedDate")]
577    signed_date: String,
578    #[serde(rename = "jwsRepresentation")]
579    jws_representation: String,
580    #[serde(rename = "verificationError")]
581    verification_error: Option<crate::error::VerificationErrorPayload>,
582    #[serde(rename = "revocationDate")]
583    revocation_date: Option<String>,
584    #[serde(rename = "revocationReason")]
585    revocation_reason: Option<String>,
586    #[serde(rename = "productType")]
587    product_type: Option<String>,
588    #[serde(rename = "appAccountToken")]
589    app_account_token: Option<String>,
590    environment: Option<String>,
591    reason: Option<String>,
592    storefront: Option<StorefrontPayload>,
593    price: Option<String>,
594    #[serde(rename = "currencyCode")]
595    currency_code: Option<String>,
596    #[serde(rename = "appTransactionID")]
597    app_transaction_id: Option<String>,
598    offer: Option<TransactionOfferPayload>,
599    #[serde(rename = "advancedCommerceInfo")]
600    advanced_commerce_info: Option<TransactionAdvancedCommerceInfoPayload>,
601    #[serde(rename = "jsonRepresentationBase64")]
602    json_representation_base64: String,
603}
604
605impl TransactionPayload {
606    fn into_transaction_parts(
607        self,
608    ) -> Result<(TransactionData, Option<TransactionAdvancedCommerceInfo>), StoreKitError> {
609        let advanced_commerce_info = self
610            .advanced_commerce_info
611            .map(TransactionAdvancedCommerceInfoPayload::into_transaction_advanced_commerce_info);
612        Ok((TransactionData {
613            id: self.id,
614            original_id: self.original_id,
615            web_order_line_item_id: self.web_order_line_item_id,
616            product_id: self.product_id,
617            subscription_group_id: self.subscription_group_id,
618            app_bundle_id: self.app_bundle_id,
619            purchase_date: self.purchase_date,
620            original_purchase_date: self.original_purchase_date,
621            expiration_date: self.expiration_date,
622            purchased_quantity: self.purchased_quantity,
623            is_upgraded: self.is_upgraded,
624            ownership_type: OwnershipType::from_raw(self.ownership_type),
625            signed_date: self.signed_date,
626            jws_representation: self.jws_representation,
627            verification_failure: self
628                .verification_error
629                .map(crate::error::VerificationFailure::from_payload),
630            revocation_date: self.revocation_date,
631            revocation_reason: self.revocation_reason.map(RevocationReason::from_raw),
632            product_type: self.product_type.map(ProductType::from_raw),
633            app_account_token: self.app_account_token,
634            environment: self.environment.map(AppStoreEnvironment::from_raw),
635            reason: self.reason.map(TransactionReason::from_raw),
636            storefront: self.storefront.map(StorefrontPayload::into_storefront),
637            price: self.price,
638            currency_code: self.currency_code,
639            app_transaction_id: self.app_transaction_id,
640            offer: self
641                .offer
642                .map(TransactionOfferPayload::into_transaction_offer),
643            json_representation: decode_base64(
644                &self.json_representation_base64,
645                "transaction JSON representation",
646            )?,
647        }, advanced_commerce_info))
648    }
649}