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