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};
22use crate::subscription_info::BillingPlanType;
23pub use crate::verification_result::VerificationResult;
24
25use crate::verification_result::VerificationResultPayload;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28/// Wraps `StoreKit.Transaction.Reason`.
29pub enum TransactionReason {
30    /// Represents the `Purchase` `StoreKit` case.
31    Purchase,
32    /// Represents the `Renewal` `StoreKit` case.
33    Renewal,
34    /// Preserves an unrecognized `StoreKit` case.
35    Unknown(String),
36}
37
38impl TransactionReason {
39    /// Returns the raw `StoreKit` string for this transaction reason.
40    pub fn as_str(&self) -> &str {
41        match self {
42            Self::Purchase => "purchase",
43            Self::Renewal => "renewal",
44            Self::Unknown(value) => value.as_str(),
45        }
46    }
47
48    fn from_raw(raw: String) -> Self {
49        match raw.as_str() {
50            "purchase" => Self::Purchase,
51            "renewal" => Self::Renewal,
52            _ => Self::Unknown(raw),
53        }
54    }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58/// Wraps `StoreKit.Transaction.RevocationReason`.
59pub enum RevocationReason {
60    /// Represents the `DeveloperIssue` `StoreKit` case.
61    DeveloperIssue,
62    /// Represents the `other` `StoreKit` case.
63    Other,
64    /// Preserves an unrecognized `StoreKit` case.
65    Unknown(String),
66}
67
68impl RevocationReason {
69    /// Returns the raw `StoreKit` string for this revocation reason.
70    pub fn as_str(&self) -> &str {
71        match self {
72            Self::DeveloperIssue => "developerIssue",
73            Self::Other => "other",
74            Self::Unknown(value) => value.as_str(),
75        }
76    }
77
78    fn from_raw(raw: String) -> Self {
79        match raw.as_str() {
80            "developerIssue" => Self::DeveloperIssue,
81            "other" => Self::Other,
82            _ => Self::Unknown(raw),
83        }
84    }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88/// Wraps `StoreKit.Transaction.RevocationType`.
89pub enum RevocationType {
90    /// Represents the `FamilyRevocation` `StoreKit` case.
91    FamilyRevocation,
92    /// Represents the `FullRefund` `StoreKit` case.
93    FullRefund,
94    /// Represents the `ProratedRefund` `StoreKit` case.
95    ProratedRefund,
96    /// Preserves an unrecognized `StoreKit` case.
97    Unknown(String),
98}
99
100impl RevocationType {
101    /// Returns the raw `StoreKit` string for this revocation type.
102    pub fn as_str(&self) -> &str {
103        match self {
104            Self::FamilyRevocation => "familyRevocation",
105            Self::FullRefund => "fullRefund",
106            Self::ProratedRefund => "proratedRefund",
107            Self::Unknown(value) => value.as_str(),
108        }
109    }
110
111    fn from_raw(raw: String) -> Self {
112        match raw.as_str() {
113            "familyRevocation" => Self::FamilyRevocation,
114            "fullRefund" => Self::FullRefund,
115            "proratedRefund" => Self::ProratedRefund,
116            _ => Self::Unknown(raw),
117        }
118    }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122/// Wraps the offer type attached to `StoreKit.Transaction`.
123pub enum OfferType {
124    /// Represents the `Introductory` `StoreKit` case.
125    Introductory,
126    /// Represents the `Promotional` `StoreKit` case.
127    Promotional,
128    /// Represents the `Code` `StoreKit` case.
129    Code,
130    /// Represents the `WinBack` `StoreKit` case.
131    WinBack,
132    /// Preserves an unrecognized `StoreKit` case.
133    Unknown(String),
134}
135
136impl OfferType {
137    /// Returns the raw `StoreKit` string for this offer type.
138    pub fn as_str(&self) -> &str {
139        match self {
140            Self::Introductory => "introductory",
141            Self::Promotional => "promotional",
142            Self::Code => "code",
143            Self::WinBack => "winBack",
144            Self::Unknown(value) => value.as_str(),
145        }
146    }
147
148    fn from_raw(raw: String) -> Self {
149        match raw.as_str() {
150            "introductory" => Self::Introductory,
151            "promotional" => Self::Promotional,
152            "code" => Self::Code,
153            "winBack" => Self::WinBack,
154            _ => Self::Unknown(raw),
155        }
156    }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160/// Wraps the offer payment mode attached to `StoreKit.Transaction`.
161pub enum OfferPaymentMode {
162    /// Represents the `FreeTrial` `StoreKit` case.
163    FreeTrial,
164    /// Represents the `PayAsYouGo` `StoreKit` case.
165    PayAsYouGo,
166    /// Represents the `PayUpFront` `StoreKit` case.
167    PayUpFront,
168    /// Represents the `OneTime` `StoreKit` case.
169    OneTime,
170    /// Preserves an unrecognized `StoreKit` case.
171    Unknown(String),
172}
173
174impl OfferPaymentMode {
175    /// Returns the raw `StoreKit` string for this offer payment mode.
176    pub fn as_str(&self) -> &str {
177        match self {
178            Self::FreeTrial => "freeTrial",
179            Self::PayAsYouGo => "payAsYouGo",
180            Self::PayUpFront => "payUpFront",
181            Self::OneTime => "oneTime",
182            Self::Unknown(value) => value.as_str(),
183        }
184    }
185
186    fn from_raw(raw: String) -> Self {
187        match raw.as_str() {
188            "freeTrial" => Self::FreeTrial,
189            "payAsYouGo" => Self::PayAsYouGo,
190            "payUpFront" => Self::PayUpFront,
191            "oneTime" => Self::OneTime,
192            _ => Self::Unknown(raw),
193        }
194    }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198/// Carries offer metadata attached to `StoreKit.Transaction`.
199pub struct TransactionOffer {
200    /// `StoreKit` identifier for this value.
201    pub id: Option<String>,
202    /// Offer type reported by `StoreKit`.
203    pub offer_type: OfferType,
204    /// Payment mode reported by `StoreKit`.
205    pub payment_mode: Option<OfferPaymentMode>,
206    /// Subscription period reported by `StoreKit`.
207    pub period: Option<SubscriptionPeriod>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
211/// Wraps `StoreKit.Transaction.CommitmentInfo`.
212pub struct TransactionCommitmentInfo {
213    /// Billing period number reported by `StoreKit`.
214    pub billing_period_number: u64,
215    /// Total billing periods reported by `StoreKit`.
216    pub total_billing_periods: u64,
217    /// Expiration date reported by `StoreKit`.
218    pub expiration_date: String,
219    /// Price reported by `StoreKit`.
220    pub price: String,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224/// Wraps `StoreKit.Transaction.OwnershipType`.
225pub enum OwnershipType {
226    /// Represents the `Purchased` `StoreKit` case.
227    Purchased,
228    /// Represents the `FamilyShared` `StoreKit` case.
229    FamilyShared,
230    /// Preserves an unrecognized `StoreKit` case.
231    Unknown(String),
232}
233
234impl OwnershipType {
235    fn from_raw(raw: String) -> Self {
236        match raw.as_str() {
237            "purchased" => Self::Purchased,
238            "familyShared" => Self::FamilyShared,
239            _ => Self::Unknown(raw),
240        }
241    }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
245/// Carries decoded fields from a `StoreKit.Transaction` payload.
246pub struct TransactionData {
247    /// `StoreKit` identifier for this value.
248    pub id: u64,
249    /// Original `StoreKit` transaction identifier.
250    pub original_id: u64,
251    /// Web order line item identifier reported by `StoreKit`.
252    pub web_order_line_item_id: Option<String>,
253    /// Product identifier reported by `StoreKit`.
254    pub product_id: String,
255    /// Subscription group identifier reported by `StoreKit`.
256    pub subscription_group_id: Option<String>,
257    /// Bundle identifier reported by `StoreKit`.
258    pub app_bundle_id: String,
259    /// Purchase date reported by `StoreKit`.
260    pub purchase_date: String,
261    /// Original purchase date reported by `StoreKit`.
262    pub original_purchase_date: String,
263    /// Expiration date reported by `StoreKit`.
264    pub expiration_date: Option<String>,
265    /// Purchased quantity reported by `StoreKit`.
266    pub purchased_quantity: u64,
267    /// Whether `StoreKit` reported `upgraded`.
268    pub is_upgraded: bool,
269    /// Ownership type reported by `StoreKit`.
270    pub ownership_type: OwnershipType,
271    /// Signature timestamp reported by `StoreKit`.
272    pub signed_date: String,
273    /// `StoreKit`-provided `jws_representation` value.
274    pub jws_representation: String,
275    /// Verification failure reported by `StoreKit`.
276    pub verification_failure: Option<VerificationFailure>,
277    /// Revocation date reported by `StoreKit`.
278    pub revocation_date: Option<String>,
279    /// Revocation reason reported by `StoreKit`.
280    pub revocation_reason: Option<RevocationReason>,
281    /// Revocation type reported by `StoreKit`.
282    pub revocation_type: Option<RevocationType>,
283    /// Product type reported by `StoreKit`.
284    pub product_type: Option<ProductType>,
285    /// App account token reported by `StoreKit`.
286    pub app_account_token: Option<String>,
287    /// Environment reported by `StoreKit`.
288    pub environment: Option<AppStoreEnvironment>,
289    /// Reason reported by `StoreKit`.
290    pub reason: Option<TransactionReason>,
291    /// Storefront metadata reported by `StoreKit`.
292    pub storefront: Option<Storefront>,
293    /// Price reported by `StoreKit`.
294    pub price: Option<String>,
295    /// Currency code reported by `StoreKit`.
296    pub currency_code: Option<String>,
297    /// Billing plan type reported by `StoreKit`.
298    pub billing_plan_type: Option<BillingPlanType>,
299    /// Commitment info reported by `StoreKit`.
300    pub commitment_info: Option<TransactionCommitmentInfo>,
301    /// App transaction identifier reported by `StoreKit`.
302    pub app_transaction_id: Option<String>,
303    /// Offer metadata reported by `StoreKit`.
304    pub offer: Option<TransactionOffer>,
305    /// Decoded JSON representation returned by `StoreKit`.
306    pub json_representation: Vec<u8>,
307}
308
309#[derive(Debug)]
310/// Wraps a live `StoreKit.Transaction` handle plus decoded payload data.
311pub struct Transaction {
312    handle: Option<NonNull<c_void>>,
313    data: TransactionData,
314    advanced_commerce_info: Option<TransactionAdvancedCommerceInfo>,
315}
316
317impl Clone for Transaction {
318    fn clone(&self) -> Self {
319        let handle = self.handle.map(|handle| {
320            // SAFETY: handle is a valid, non-null StoreKit transaction pointer maintained
321            // by this Transaction.  sk_transaction_retain increments the retain count
322            // and returns the same pointer (never null for a live transaction).
323            let retained = unsafe { ffi::sk_transaction_retain(handle.as_ptr()) };
324            NonNull::new(retained).expect("StoreKit transaction retain returned null")
325        });
326        Self {
327            handle,
328            data: self.data.clone(),
329            advanced_commerce_info: self.advanced_commerce_info.clone(),
330        }
331    }
332}
333
334impl Drop for Transaction {
335    fn drop(&mut self) {
336        if let Some(handle) = self.handle {
337            // SAFETY: handle is a valid, non-null StoreKit transaction pointer that
338            // this Transaction uniquely owns (or co-owns with a matching retain from
339            // Clone).  Drop is the unique release point per ownership token.
340            unsafe { ffi::sk_transaction_release(handle.as_ptr()) };
341        }
342    }
343}
344
345impl Transaction {
346    /// Creates a stream backed by `StoreKit.Transaction.currentEntitlements`.
347    pub fn current_entitlements() -> Result<TransactionStream, StoreKitError> {
348        TransactionStream::new(&TransactionStreamConfig::current_entitlements())
349    }
350
351    /// Creates a stream backed by `StoreKit.Transaction.all`.
352    pub fn all() -> Result<TransactionStream, StoreKitError> {
353        TransactionStream::new(&TransactionStreamConfig::all())
354    }
355
356    /// Creates a stream backed by `StoreKit.Transaction.updates`.
357    pub fn updates() -> Result<TransactionStream, StoreKitError> {
358        TransactionStream::new(&TransactionStreamConfig::updates())
359    }
360
361    /// Creates a stream backed by `StoreKit.Transaction.unfinished`.
362    pub fn unfinished() -> Result<TransactionStream, StoreKitError> {
363        TransactionStream::new(&TransactionStreamConfig::unfinished())
364    }
365
366    /// Creates a stream of all `StoreKit` transactions for the supplied product identifier.
367    pub fn all_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
368        TransactionStream::new(&TransactionStreamConfig::all_for(product_id))
369    }
370
371    /// Creates a stream of current `StoreKit` entitlements for the supplied product identifier.
372    pub fn current_entitlements_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
373        TransactionStream::new(&TransactionStreamConfig::current_entitlements_for(
374            product_id,
375        ))
376    }
377
378    /// Fetches the latest `StoreKit` transaction for the supplied product identifier.
379    pub fn latest_for(product_id: &str) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
380        let product_id = cstring_from_str(product_id, "product id")?;
381        let mut transaction_handle = ptr::null_mut();
382        let mut result_json = ptr::null_mut();
383        let mut error_message = ptr::null_mut();
384        let status = unsafe {
385            ffi::sk_transaction_latest_for(
386                product_id.as_ptr(),
387                &mut transaction_handle,
388                &mut result_json,
389                &mut error_message,
390            )
391        };
392        if status != ffi::status::OK {
393            return Err(unsafe { error_from_status(status, error_message) });
394        }
395
396        let payload = unsafe {
397            parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
398                result_json,
399                "latest transaction",
400            )
401        }?;
402        payload
403            .map(|payload| {
404                payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
405            })
406            .transpose()
407    }
408
409    /// Fetches the current `StoreKit` entitlement transaction for the supplied product identifier.
410    pub fn current_entitlement_for(
411        product_id: &str,
412    ) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
413        let product_id = cstring_from_str(product_id, "product id")?;
414        let mut transaction_handle = ptr::null_mut();
415        let mut result_json = ptr::null_mut();
416        let mut error_message = ptr::null_mut();
417        let status = unsafe {
418            ffi::sk_transaction_current_entitlement_for(
419                product_id.as_ptr(),
420                &mut transaction_handle,
421                &mut result_json,
422                &mut error_message,
423            )
424        };
425        if status != ffi::status::OK {
426            return Err(unsafe { error_from_status(status, error_message) });
427        }
428
429        let payload = unsafe {
430            parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
431                result_json,
432                "current entitlement transaction",
433            )
434        }?;
435        payload
436            .map(|payload| {
437                payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
438            })
439            .transpose()
440    }
441
442    /// Returns the decoded `StoreKit.Transaction` payload data.
443    pub const fn data(&self) -> &TransactionData {
444        &self.data
445    }
446
447    /// Returns advanced-commerce metadata attached to this `StoreKit` transaction.
448    pub const fn advanced_commerce_info(&self) -> Option<&TransactionAdvancedCommerceInfo> {
449        self.advanced_commerce_info.as_ref()
450    }
451
452    /// Returns whether this wrapper still owns a live `StoreKit` handle.
453    pub const fn has_live_handle(&self) -> bool {
454        self.handle.is_some()
455    }
456
457    /// Asks `StoreKit` to verify this transaction again.
458    pub fn verify(&self) -> Result<(), StoreKitError> {
459        self.handle.map_or_else(
460            || {
461                self.data
462                    .verification_failure
463                    .clone()
464                    .map_or(Ok(()), |failure| Err(StoreKitError::Verification(failure)))
465            },
466            |handle| {
467                let mut error_message = ptr::null_mut();
468                let status =
469                    unsafe { ffi::sk_transaction_verify(handle.as_ptr(), &mut error_message) };
470                if status == ffi::status::OK {
471                    Ok(())
472                } else {
473                    Err(unsafe { error_from_status(status, error_message) })
474                }
475            },
476        )
477    }
478
479    /// Calls `StoreKit.Transaction.finish()`.
480    pub fn finish(&self) -> Result<(), StoreKitError> {
481        self.handle.map_or_else(
482            || {
483                Err(StoreKitError::NotSupported(
484                    "transaction snapshots cannot be finished because they do not carry a live StoreKit handle"
485                        .to_owned(),
486                ))
487            },
488            |handle| {
489                let mut error_message = ptr::null_mut();
490                let status = unsafe { ffi::sk_transaction_finish(handle.as_ptr(), &mut error_message) };
491                if status == ffi::status::OK {
492                    Ok(())
493                } else {
494                    Err(unsafe { error_from_status(status, error_message) })
495                }
496            },
497        )
498    }
499
500    /// Begins a `StoreKit` refund request for this transaction.
501    pub fn begin_refund_request(&self) -> Result<RefundRequestStatus, StoreKitError> {
502        Refund::begin_for_transaction_id(self.data.id)
503    }
504
505    pub(crate) fn from_raw_parts(
506        handle: *mut c_void,
507        payload: TransactionPayload,
508    ) -> Result<Self, StoreKitError> {
509        let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
510        Ok(Self {
511            handle: NonNull::new(handle),
512            data,
513            advanced_commerce_info,
514        })
515    }
516
517    pub(crate) fn from_snapshot_payload(
518        payload: TransactionPayload,
519    ) -> Result<Self, StoreKitError> {
520        let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
521        Ok(Self {
522            handle: None,
523            data,
524            advanced_commerce_info,
525        })
526    }
527}
528
529#[derive(Debug)]
530/// Wraps the `StoreKit` transaction stream APIs.
531pub struct TransactionStream {
532    handle: NonNull<c_void>,
533    finished: bool,
534}
535
536impl Drop for TransactionStream {
537    fn drop(&mut self) {
538        unsafe { ffi::sk_transaction_stream_release(self.handle.as_ptr()) };
539    }
540}
541
542impl TransactionStream {
543    fn new(config: &TransactionStreamConfig) -> Result<Self, StoreKitError> {
544        let config_json = json_cstring(config, "transaction stream config")?;
545        let mut error_message = ptr::null_mut();
546        let handle =
547            unsafe { ffi::sk_transaction_stream_create(config_json.as_ptr(), &mut error_message) };
548        let handle = NonNull::new(handle)
549            .ok_or_else(|| unsafe { error_from_status(ffi::status::UNKNOWN, error_message) })?;
550        Ok(Self {
551            handle,
552            finished: false,
553        })
554    }
555
556    /// Returns whether this `StoreKit` stream has reached the end of the sequence.
557    pub const fn is_finished(&self) -> bool {
558        self.finished
559    }
560
561    #[allow(clippy::should_implement_trait)]
562    /// Waits for the next value from the `StoreKit` stream using the default timeout.
563    pub fn next(&mut self) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
564        self.next_timeout(Duration::from_secs(30))
565    }
566
567    /// Waits for the next value from the `StoreKit` stream up to the supplied timeout.
568    pub fn next_timeout(
569        &mut self,
570        timeout: Duration,
571    ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
572        let mut transaction_handle = ptr::null_mut();
573        let mut verification_json = ptr::null_mut();
574        let mut error_message = ptr::null_mut();
575        let status = unsafe {
576            ffi::sk_transaction_stream_next(
577                self.handle.as_ptr(),
578                duration_to_timeout_ms(timeout),
579                &mut transaction_handle,
580                &mut verification_json,
581                &mut error_message,
582            )
583        };
584
585        match status {
586            ffi::status::OK => {
587                let payload = unsafe {
588                    parse_json_ptr::<VerificationResultPayload<TransactionPayload>>(
589                        verification_json,
590                        "transaction verification result",
591                    )
592                };
593                match payload {
594                    Ok(payload) => payload
595                        .into_result(|payload| {
596                            Transaction::from_raw_parts(transaction_handle, payload)
597                        })
598                        .map(Some),
599                    Err(error) => {
600                        if !transaction_handle.is_null() {
601                            unsafe { ffi::sk_transaction_release(transaction_handle) };
602                        }
603                        Err(error)
604                    }
605                }
606            }
607            ffi::status::END_OF_STREAM => {
608                self.finished = true;
609                Ok(None)
610            }
611            ffi::status::TIMED_OUT => Ok(None),
612            _ => Err(unsafe { error_from_status(status, error_message) }),
613        }
614    }
615}
616
617#[derive(Debug, Serialize)]
618struct TransactionStreamConfig {
619    kind: &'static str,
620    #[serde(rename = "productID", skip_serializing_if = "Option::is_none")]
621    product_id: Option<String>,
622}
623
624impl TransactionStreamConfig {
625    const fn all() -> Self {
626        Self {
627            kind: "all",
628            product_id: None,
629        }
630    }
631
632    const fn current_entitlements() -> Self {
633        Self {
634            kind: "currentEntitlements",
635            product_id: None,
636        }
637    }
638
639    const fn updates() -> Self {
640        Self {
641            kind: "updates",
642            product_id: None,
643        }
644    }
645
646    const fn unfinished() -> Self {
647        Self {
648            kind: "unfinished",
649            product_id: None,
650        }
651    }
652
653    fn all_for(product_id: &str) -> Self {
654        Self {
655            kind: "allFor",
656            product_id: Some(product_id.to_owned()),
657        }
658    }
659
660    fn current_entitlements_for(product_id: &str) -> Self {
661        Self {
662            kind: "currentEntitlementsFor",
663            product_id: Some(product_id.to_owned()),
664        }
665    }
666}
667
668#[derive(Debug, Deserialize)]
669pub(crate) struct TransactionOfferPayload {
670    id: Option<String>,
671    #[serde(rename = "type")]
672    offer_type: String,
673    #[serde(rename = "paymentMode")]
674    payment_mode: Option<String>,
675    period: Option<SubscriptionPeriodPayload>,
676}
677
678impl TransactionOfferPayload {
679    pub(crate) fn into_transaction_offer(self) -> TransactionOffer {
680        TransactionOffer {
681            id: self.id,
682            offer_type: OfferType::from_raw(self.offer_type),
683            payment_mode: self.payment_mode.map(OfferPaymentMode::from_raw),
684            period: self
685                .period
686                .map(SubscriptionPeriodPayload::into_subscription_period),
687        }
688    }
689}
690
691#[derive(Debug, Deserialize)]
692pub(crate) struct TransactionCommitmentInfoPayload {
693    #[serde(rename = "billingPeriodNumber")]
694    billing_period_number: u64,
695    #[serde(rename = "totalBillingPeriods")]
696    total_billing_periods: u64,
697    #[serde(rename = "expirationDate")]
698    expiration_date: String,
699    price: String,
700}
701
702impl TransactionCommitmentInfoPayload {
703    pub(crate) fn into_transaction_commitment_info(self) -> TransactionCommitmentInfo {
704        TransactionCommitmentInfo {
705            billing_period_number: self.billing_period_number,
706            total_billing_periods: self.total_billing_periods,
707            expiration_date: self.expiration_date,
708            price: self.price,
709        }
710    }
711}
712
713#[derive(Debug, Deserialize)]
714pub(crate) struct TransactionPayload {
715    id: u64,
716    #[serde(rename = "originalID")]
717    original_id: u64,
718    #[serde(rename = "webOrderLineItemID")]
719    web_order_line_item_id: Option<String>,
720    #[serde(rename = "productID")]
721    product_id: String,
722    #[serde(rename = "subscriptionGroupID")]
723    subscription_group_id: Option<String>,
724    #[serde(rename = "appBundleID")]
725    app_bundle_id: String,
726    #[serde(rename = "purchaseDate")]
727    purchase_date: String,
728    #[serde(rename = "originalPurchaseDate")]
729    original_purchase_date: String,
730    #[serde(rename = "expirationDate")]
731    expiration_date: Option<String>,
732    #[serde(rename = "purchasedQuantity")]
733    purchased_quantity: u64,
734    #[serde(rename = "isUpgraded")]
735    is_upgraded: bool,
736    #[serde(rename = "ownershipType")]
737    ownership_type: String,
738    #[serde(rename = "signedDate")]
739    signed_date: String,
740    #[serde(rename = "jwsRepresentation")]
741    jws_representation: String,
742    #[serde(rename = "verificationError")]
743    verification_error: Option<crate::error::VerificationErrorPayload>,
744    #[serde(rename = "revocationDate")]
745    revocation_date: Option<String>,
746    #[serde(rename = "revocationReason")]
747    revocation_reason: Option<String>,
748    #[serde(rename = "revocationType")]
749    revocation_type: Option<String>,
750    #[serde(rename = "productType")]
751    product_type: Option<String>,
752    #[serde(rename = "appAccountToken")]
753    app_account_token: Option<String>,
754    environment: Option<String>,
755    reason: Option<String>,
756    storefront: Option<StorefrontPayload>,
757    price: Option<String>,
758    #[serde(rename = "currencyCode")]
759    currency_code: Option<String>,
760    #[serde(rename = "billingPlanType")]
761    billing_plan_type: Option<String>,
762    #[serde(rename = "commitmentInfo")]
763    commitment_info: Option<TransactionCommitmentInfoPayload>,
764    #[serde(rename = "appTransactionID")]
765    app_transaction_id: Option<String>,
766    offer: Option<TransactionOfferPayload>,
767    #[serde(rename = "advancedCommerceInfo")]
768    advanced_commerce_info: Option<TransactionAdvancedCommerceInfoPayload>,
769    #[serde(rename = "jsonRepresentationBase64")]
770    json_representation_base64: String,
771}
772
773impl TransactionPayload {
774    fn into_transaction_parts(
775        self,
776    ) -> Result<(TransactionData, Option<TransactionAdvancedCommerceInfo>), StoreKitError> {
777        let advanced_commerce_info = self
778            .advanced_commerce_info
779            .map(TransactionAdvancedCommerceInfoPayload::into_transaction_advanced_commerce_info);
780        Ok((
781            TransactionData {
782                id: self.id,
783                original_id: self.original_id,
784                web_order_line_item_id: self.web_order_line_item_id,
785                product_id: self.product_id,
786                subscription_group_id: self.subscription_group_id,
787                app_bundle_id: self.app_bundle_id,
788                purchase_date: self.purchase_date,
789                original_purchase_date: self.original_purchase_date,
790                expiration_date: self.expiration_date,
791                purchased_quantity: self.purchased_quantity,
792                is_upgraded: self.is_upgraded,
793                ownership_type: OwnershipType::from_raw(self.ownership_type),
794                signed_date: self.signed_date,
795                jws_representation: self.jws_representation,
796                verification_failure: self
797                    .verification_error
798                    .map(crate::error::VerificationFailure::from_payload),
799                revocation_date: self.revocation_date,
800                revocation_reason: self.revocation_reason.map(RevocationReason::from_raw),
801                revocation_type: self.revocation_type.map(RevocationType::from_raw),
802                product_type: self.product_type.map(ProductType::from_raw),
803                app_account_token: self.app_account_token,
804                environment: self.environment.map(AppStoreEnvironment::from_raw),
805                reason: self.reason.map(TransactionReason::from_raw),
806                storefront: self.storefront.map(StorefrontPayload::into_storefront),
807                price: self.price,
808                currency_code: self.currency_code,
809                billing_plan_type: self.billing_plan_type.map(BillingPlanType::from_raw),
810                commitment_info: self
811                    .commitment_info
812                    .map(TransactionCommitmentInfoPayload::into_transaction_commitment_info),
813                app_transaction_id: self.app_transaction_id,
814                offer: self
815                    .offer
816                    .map(TransactionOfferPayload::into_transaction_offer),
817                json_representation: decode_base64(
818                    &self.json_representation_base64,
819                    "transaction JSON representation",
820                )?,
821            },
822            advanced_commerce_info,
823        ))
824    }
825}