Skip to main content

storekit/
advanced_commerce.rs

1use core::ffi::c_void;
2use core::ptr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::app_store::AppStore;
7use crate::error::StoreKitError;
8use crate::ffi;
9use crate::private::{cstring_from_str, error_from_status, json_cstring, parse_json_ptr};
10use crate::product::ProductType;
11use crate::purchase_option::{PurchaseResult, PurchaseResultPayload};
12use crate::renewal_info::RenewalInfo;
13use crate::subscription::{SubscriptionPeriod, SubscriptionPeriodPayload};
14use crate::transaction::{Transaction, TransactionStream};
15use crate::verification_result::VerificationResult;
16use crate::window::NSWindowHandle;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19#[serde(tag = "kind", rename_all = "camelCase")]
20/// Represents the merchandising kind requested from `StoreKit.AppStore`.
21pub enum AppStoreMerchandisingKind {
22    /// Represents the `SubscriptionBundle` `StoreKit` case.
23    SubscriptionBundle {
24        #[serde(rename = "groupID")]
25        /// Subscription group identifier reported by `StoreKit`.
26        group_id: String,
27    },
28}
29
30impl AppStoreMerchandisingKind {
31    /// Builds the `StoreKit` merchandising request for a subscription bundle.
32    pub fn subscription_bundle(group_id: impl Into<String>) -> Self {
33        Self::SubscriptionBundle {
34            group_id: group_id.into(),
35        }
36    }
37}
38
39#[derive(Debug)]
40#[allow(clippy::large_enum_variant)]
41/// Represents the result returned by `StoreKit` merchandising presentation APIs.
42pub enum AppStoreMerchandisingPresentationResult {
43    /// Represents the `Dismissed` `StoreKit` case.
44    Dismissed,
45    /// The merchandising flow completed with a purchase result.
46    PurchaseCompleted(PurchaseResult),
47}
48
49impl AppStore {
50    /// Fetches the age rating code reported by `StoreKit`.
51    pub fn age_rating_code() -> Result<Option<i64>, StoreKitError> {
52        let mut raw_value = 0_i64;
53        let mut has_value = 0;
54        let mut error_message = ptr::null_mut();
55        let status = unsafe {
56            ffi::sk_app_store_age_rating_code(&mut raw_value, &mut has_value, &mut error_message)
57        };
58        if status == ffi::status::OK {
59            Ok((has_value != 0).then_some(raw_value))
60        } else {
61            Err(unsafe { error_from_status(status, error_message) })
62        }
63    }
64
65    /// Presents `StoreKit` merchandising UI in the supplied window.
66    pub fn present_merchandising(
67        kind: &AppStoreMerchandisingKind,
68        window: &NSWindowHandle,
69    ) -> Result<AppStoreMerchandisingPresentationResult, StoreKitError> {
70        let kind_json = json_cstring(kind, "App Store merchandising kind")?;
71        let mut transaction_handle: *mut c_void = ptr::null_mut();
72        let mut result_json = ptr::null_mut();
73        let mut error_message = ptr::null_mut();
74        let status = unsafe {
75            ffi::sk_app_store_present_merchandising(
76                kind_json.as_ptr(),
77                window.as_raw(),
78                &mut transaction_handle,
79                &mut result_json,
80                &mut error_message,
81            )
82        };
83        if status != ffi::status::OK {
84            return Err(unsafe { error_from_status(status, error_message) });
85        }
86        let payload = unsafe {
87            parse_json_ptr::<AppStoreMerchandisingPresentationResultPayload>(
88                result_json,
89                "App Store merchandising presentation result",
90            )
91        }?;
92        payload.into_result(transaction_handle)
93    }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
97#[serde(tag = "kind", rename_all = "camelCase")]
98/// Represents advanced-commerce options forwarded to `StoreKit` purchase APIs.
99pub enum AdvancedCommercePurchaseOption {
100    /// Represents the `OnStorefrontChange` `StoreKit` case.
101    OnStorefrontChange {
102        #[serde(rename = "shouldContinuePurchase")]
103        /// Value forwarded to `StoreKit` for `should_continue_purchase`.
104        should_continue_purchase: bool,
105    },
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109/// Wraps a `StoreKit` advanced-commerce product.
110pub struct AdvancedCommerceProduct {
111    /// `StoreKit` identifier for this value.
112    pub id: String,
113    /// Product type reported by `StoreKit`.
114    pub product_type: ProductType,
115}
116
117impl AdvancedCommerceProduct {
118    /// Fetches the `StoreKit` advanced-commerce product for the supplied identifier.
119    pub fn new(id: &str) -> Result<Self, StoreKitError> {
120        let product_id = cstring_from_str(id, "advanced commerce product id")?;
121        let mut product_json = ptr::null_mut();
122        let mut error_message = ptr::null_mut();
123        let status = unsafe {
124            ffi::sk_advanced_commerce_product_json(
125                product_id.as_ptr(),
126                &mut product_json,
127                &mut error_message,
128            )
129        };
130        if status != ffi::status::OK {
131            return Err(unsafe { error_from_status(status, error_message) });
132        }
133        let payload = unsafe {
134            parse_json_ptr::<AdvancedCommerceProductPayload>(
135                product_json,
136                "advanced commerce product",
137            )
138        }?;
139        Ok(payload.into_product())
140    }
141
142    /// Calls the `StoreKit` advanced-commerce purchase API while presenting UI in the supplied `NSWindow`.
143    pub fn purchase_in_window(
144        &self,
145        compact_jws: &str,
146        window: &NSWindowHandle,
147        options: &[AdvancedCommercePurchaseOption],
148    ) -> Result<PurchaseResult, StoreKitError> {
149        let product_id = cstring_from_str(&self.id, "advanced commerce product id")?;
150        let compact_jws = cstring_from_str(compact_jws, "advanced commerce compact JWS")?;
151        let options_json = json_cstring(options, "advanced commerce purchase options")?;
152        let mut transaction_handle: *mut c_void = ptr::null_mut();
153        let mut result_json = ptr::null_mut();
154        let mut error_message = ptr::null_mut();
155        let status = unsafe {
156            ffi::sk_advanced_commerce_product_purchase(
157                product_id.as_ptr(),
158                compact_jws.as_ptr(),
159                window.as_raw(),
160                options_json.as_ptr(),
161                &mut transaction_handle,
162                &mut result_json,
163                &mut error_message,
164            )
165        };
166        if status != ffi::status::OK {
167            return Err(unsafe { error_from_status(status, error_message) });
168        }
169
170        let payload = unsafe {
171            parse_json_ptr::<PurchaseResultPayload>(
172                result_json,
173                "advanced commerce purchase result",
174            )
175        };
176        match payload {
177            Ok(payload) => payload.into_purchase_result(transaction_handle),
178            Err(error) => {
179                if !transaction_handle.is_null() {
180                    unsafe { ffi::sk_transaction_release(transaction_handle) };
181                }
182                Err(error)
183            }
184        }
185    }
186
187    /// Fetches the latest `StoreKit` transaction for this advanced-commerce product.
188    pub fn latest_transaction(
189        &self,
190    ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
191        Transaction::latest_for(&self.id)
192    }
193
194    /// Creates a stream of all `StoreKit` transactions for this advanced-commerce product.
195    pub fn all_transactions(&self) -> Result<TransactionStream, StoreKitError> {
196        Transaction::all_for(&self.id)
197    }
198
199    /// Creates a stream of current `StoreKit` entitlements for this advanced-commerce product.
200    pub fn current_entitlements(&self) -> Result<TransactionStream, StoreKitError> {
201        Transaction::current_entitlements_for(&self.id)
202    }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
206/// Carries advanced-commerce metadata attached to `StoreKit.Transaction`.
207pub struct TransactionAdvancedCommerceInfo {
208    /// Request reference identifier reported by `StoreKit`.
209    pub request_reference_id: String,
210    /// Estimated tax reported by `StoreKit`.
211    pub estimated_tax: String,
212    /// Tax rate reported by `StoreKit`.
213    pub tax_rate: String,
214    /// Tax code reported by `StoreKit`.
215    pub tax_code: String,
216    /// Tax-exclusive price reported by `StoreKit`.
217    pub tax_exclusive_price: String,
218    /// Description reported by `StoreKit`.
219    pub description: Option<String>,
220    /// Display name reported by `StoreKit`.
221    pub display_name: Option<String>,
222    /// Subscription period reported by `StoreKit`.
223    pub period: Option<SubscriptionPeriod>,
224    /// Nested items reported by `StoreKit`.
225    pub items: Vec<TransactionAdvancedCommerceItem>,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
229/// Carries an advanced-commerce item attached to `StoreKit.Transaction`.
230pub struct TransactionAdvancedCommerceItem {
231    /// Detailed item payload reported by `StoreKit`.
232    pub details: TransactionAdvancedCommerceItemDetails,
233    /// Refund details reported by `StoreKit`.
234    pub refunds: Option<Vec<TransactionAdvancedCommerceRefund>>,
235    /// Revocation date reported by `StoreKit`.
236    pub revocation_date: Option<String>,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
240/// Carries detail fields for an advanced-commerce item returned by `StoreKit`.
241pub struct TransactionAdvancedCommerceItemDetails {
242    /// SKU reported by `StoreKit`.
243    pub sku: String,
244    /// Display name reported by `StoreKit`.
245    pub display_name: String,
246    /// Description reported by `StoreKit`.
247    pub description: String,
248    /// Offer metadata reported by `StoreKit`.
249    pub offer: Option<TransactionAdvancedCommerceOffer>,
250    /// Price reported by `StoreKit`.
251    pub price: String,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
255/// Carries advanced-commerce offer data attached to `StoreKit.Transaction`.
256pub struct TransactionAdvancedCommerceOffer {
257    /// Price reported by `StoreKit`.
258    pub price: String,
259    /// Subscription period reported by `StoreKit`.
260    pub period: SubscriptionPeriod,
261    /// Number of periods reported by `StoreKit`.
262    pub period_count: i64,
263    /// Reason reported by `StoreKit`.
264    pub reason: TransactionAdvancedCommerceOfferReason,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
268/// Wraps advanced-commerce offer reasons returned by `StoreKit`.
269pub enum TransactionAdvancedCommerceOfferReason {
270    /// Represents the `Acquisition` `StoreKit` case.
271    Acquisition,
272    /// Represents the `Retention` `StoreKit` case.
273    Retention,
274    /// Represents the `WinBack` `StoreKit` case.
275    WinBack,
276    /// Preserves an unrecognized `StoreKit` case.
277    Unknown(String),
278}
279
280impl TransactionAdvancedCommerceOfferReason {
281    /// Returns the raw `StoreKit` string for this advanced-commerce offer reason.
282    pub fn as_str(&self) -> &str {
283        match self {
284            Self::Acquisition => "acquisition",
285            Self::Retention => "retention",
286            Self::WinBack => "winBack",
287            Self::Unknown(value) => value.as_str(),
288        }
289    }
290
291    fn from_raw(raw: String) -> Self {
292        match raw.as_str() {
293            "acquisition" => Self::Acquisition,
294            "retention" => Self::Retention,
295            "winBack" => Self::WinBack,
296            _ => Self::Unknown(raw),
297        }
298    }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
302/// Carries advanced-commerce refund details returned by `StoreKit`.
303pub struct TransactionAdvancedCommerceRefund {
304    /// Reason reported by `StoreKit`.
305    pub reason: TransactionAdvancedCommerceRefundReason,
306    /// Refund type reported by `StoreKit`.
307    pub refund_type: TransactionAdvancedCommerceRefundType,
308    /// Date reported by `StoreKit`.
309    pub date: String,
310    /// Amount reported by `StoreKit`.
311    pub amount: String,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
315/// Wraps advanced-commerce refund reasons returned by `StoreKit`.
316pub enum TransactionAdvancedCommerceRefundReason {
317    /// Represents the `Legal` `StoreKit` case.
318    Legal,
319    /// Represents the `ModifyItems` `StoreKit` case.
320    ModifyItems,
321    /// Represents the `Unintended` `StoreKit` case.
322    Unintended,
323    /// Represents the `Unfulfilled` `StoreKit` case.
324    Unfulfilled,
325    /// Represents the `Unsatisfied` `StoreKit` case.
326    Unsatisfied,
327    /// Represents the `other` `StoreKit` case.
328    Other,
329    /// Preserves an unrecognized `StoreKit` case.
330    Unknown(String),
331}
332
333impl TransactionAdvancedCommerceRefundReason {
334    /// Returns the raw `StoreKit` string for this advanced-commerce refund reason.
335    pub fn as_str(&self) -> &str {
336        match self {
337            Self::Legal => "legal",
338            Self::ModifyItems => "modifyItems",
339            Self::Unintended => "unintended",
340            Self::Unfulfilled => "unfulfilled",
341            Self::Unsatisfied => "unsatisfied",
342            Self::Other => "other",
343            Self::Unknown(value) => value.as_str(),
344        }
345    }
346
347    fn from_raw(raw: String) -> Self {
348        match raw.as_str() {
349            "legal" => Self::Legal,
350            "modifyItems" => Self::ModifyItems,
351            "unintended" => Self::Unintended,
352            "unfulfilled" => Self::Unfulfilled,
353            "unsatisfied" => Self::Unsatisfied,
354            "other" => Self::Other,
355            _ => Self::Unknown(raw),
356        }
357    }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361/// Wraps advanced-commerce refund types returned by `StoreKit`.
362pub enum TransactionAdvancedCommerceRefundType {
363    /// Represents the `Custom` `StoreKit` case.
364    Custom,
365    /// Represents the `ProRated` `StoreKit` case.
366    ProRated,
367    /// Represents the `Full` `StoreKit` case.
368    Full,
369    /// Preserves an unrecognized `StoreKit` case.
370    Unknown(String),
371}
372
373impl TransactionAdvancedCommerceRefundType {
374    /// Returns the raw `StoreKit` string for this advanced-commerce refund type.
375    pub fn as_str(&self) -> &str {
376        match self {
377            Self::Custom => "custom",
378            Self::ProRated => "proRated",
379            Self::Full => "full",
380            Self::Unknown(value) => value.as_str(),
381        }
382    }
383
384    fn from_raw(raw: String) -> Self {
385        match raw.as_str() {
386            "custom" => Self::Custom,
387            "proRated" => Self::ProRated,
388            "full" => Self::Full,
389            _ => Self::Unknown(raw),
390        }
391    }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
395/// Carries advanced-commerce metadata attached to `StoreKit.RenewalInfo`.
396pub struct RenewalInfoAdvancedCommerceInfo {
397    /// Consistency token reported by `StoreKit`.
398    pub consistency_token: String,
399    /// Request reference identifier reported by `StoreKit`.
400    pub request_reference_id: String,
401    /// Tax code reported by `StoreKit`.
402    pub tax_code: String,
403    /// Description reported by `StoreKit`.
404    pub description: String,
405    /// Display name reported by `StoreKit`.
406    pub display_name: String,
407    /// Subscription period reported by `StoreKit`.
408    pub period: SubscriptionPeriod,
409    /// Nested items reported by `StoreKit`.
410    pub items: Vec<RenewalInfoAdvancedCommerceItem>,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq)]
414/// Carries an advanced-commerce renewal item returned by `StoreKit`.
415pub struct RenewalInfoAdvancedCommerceItem {
416    /// Detailed item payload reported by `StoreKit`.
417    pub details: TransactionAdvancedCommerceItemDetails,
418    /// Price increase details reported by `StoreKit`.
419    pub price_increase_info: Option<RenewalInfoAdvancedCommercePriceIncreaseInfo>,
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
423/// Carries advanced-commerce price increase details returned by `StoreKit`.
424pub struct RenewalInfoAdvancedCommercePriceIncreaseInfo {
425    /// Status reported by `StoreKit`.
426    pub status: RenewalInfoAdvancedCommercePriceIncreaseStatus,
427    /// Price reported by `StoreKit`.
428    pub price: String,
429    /// Dependent SKUs reported by `StoreKit`.
430    pub dependent_skus: Vec<String>,
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
434/// Wraps advanced-commerce price increase states returned by `StoreKit`.
435pub enum RenewalInfoAdvancedCommercePriceIncreaseStatus {
436    /// The `StoreKit` flow is pending further action.
437    Pending,
438    /// Represents the `Accepted` `StoreKit` case.
439    Accepted,
440    /// Represents the `Scheduled` `StoreKit` case.
441    Scheduled,
442    /// Preserves an unrecognized `StoreKit` case.
443    Unknown(String),
444}
445
446impl RenewalInfoAdvancedCommercePriceIncreaseStatus {
447    /// Returns the raw `StoreKit` string for this advanced-commerce price increase status.
448    pub fn as_str(&self) -> &str {
449        match self {
450            Self::Pending => "pending",
451            Self::Accepted => "accepted",
452            Self::Scheduled => "scheduled",
453            Self::Unknown(value) => value.as_str(),
454        }
455    }
456
457    fn from_raw(raw: String) -> Self {
458        match raw.as_str() {
459            "pending" => Self::Pending,
460            "accepted" => Self::Accepted,
461            "scheduled" => Self::Scheduled,
462            _ => Self::Unknown(raw),
463        }
464    }
465}
466
467impl VerificationResult<Transaction> {
468    /// Returns advanced-commerce metadata decoded from the `StoreKit.Transaction` payload.
469    pub fn advanced_commerce_info(
470        &self,
471    ) -> Result<Option<TransactionAdvancedCommerceInfo>, StoreKitError> {
472        parse_transaction_advanced_commerce_info_payload(&self.metadata().payload_data)
473    }
474}
475
476impl VerificationResult<RenewalInfo> {
477    /// Returns advanced-commerce metadata decoded from the `StoreKit.RenewalInfo` payload.
478    pub fn advanced_commerce_info(
479        &self,
480    ) -> Result<Option<RenewalInfoAdvancedCommerceInfo>, StoreKitError> {
481        parse_renewal_advanced_commerce_info_payload(&self.metadata().payload_data)
482    }
483}
484
485#[derive(Debug, Deserialize)]
486pub(crate) struct TransactionAdvancedCommerceInfoPayload {
487    #[serde(rename = "requestReferenceID")]
488    request_reference_id: String,
489    #[serde(rename = "estimatedTax")]
490    estimated_tax: String,
491    #[serde(rename = "taxRate")]
492    tax_rate: String,
493    #[serde(rename = "taxCode")]
494    tax_code: String,
495    #[serde(rename = "taxExclusivePrice")]
496    tax_exclusive_price: String,
497    description: Option<String>,
498    #[serde(rename = "displayName")]
499    display_name: Option<String>,
500    period: Option<SubscriptionPeriodPayload>,
501    items: Vec<TransactionAdvancedCommerceItemPayload>,
502}
503
504impl TransactionAdvancedCommerceInfoPayload {
505    pub(crate) fn into_transaction_advanced_commerce_info(self) -> TransactionAdvancedCommerceInfo {
506        TransactionAdvancedCommerceInfo {
507            request_reference_id: self.request_reference_id,
508            estimated_tax: self.estimated_tax,
509            tax_rate: self.tax_rate,
510            tax_code: self.tax_code,
511            tax_exclusive_price: self.tax_exclusive_price,
512            description: self.description,
513            display_name: self.display_name,
514            period: self
515                .period
516                .map(SubscriptionPeriodPayload::into_subscription_period),
517            items: self
518                .items
519                .into_iter()
520                .map(
521                    TransactionAdvancedCommerceItemPayload::into_transaction_advanced_commerce_item,
522                )
523                .collect(),
524        }
525    }
526}
527
528#[derive(Debug, Deserialize)]
529struct TransactionAdvancedCommerceItemPayload {
530    details: TransactionAdvancedCommerceItemDetailsPayload,
531    refunds: Option<Vec<TransactionAdvancedCommerceRefundPayload>>,
532    #[serde(rename = "revocationDate")]
533    revocation_date: Option<String>,
534}
535
536impl TransactionAdvancedCommerceItemPayload {
537    fn into_transaction_advanced_commerce_item(self) -> TransactionAdvancedCommerceItem {
538        TransactionAdvancedCommerceItem {
539            details: self.details.into_transaction_advanced_commerce_item_details(),
540            refunds: self.refunds.map(|refunds| {
541                refunds
542                    .into_iter()
543                    .map(TransactionAdvancedCommerceRefundPayload::into_transaction_advanced_commerce_refund)
544                    .collect()
545            }),
546            revocation_date: self.revocation_date,
547        }
548    }
549}
550
551#[derive(Debug, Deserialize)]
552struct TransactionAdvancedCommerceItemDetailsPayload {
553    sku: String,
554    #[serde(rename = "displayName")]
555    display_name: String,
556    description: String,
557    offer: Option<TransactionAdvancedCommerceOfferPayload>,
558    price: String,
559}
560
561impl TransactionAdvancedCommerceItemDetailsPayload {
562    fn into_transaction_advanced_commerce_item_details(
563        self,
564    ) -> TransactionAdvancedCommerceItemDetails {
565        TransactionAdvancedCommerceItemDetails {
566            sku: self.sku,
567            display_name: self.display_name,
568            description: self.description,
569            offer: self.offer.map(
570                TransactionAdvancedCommerceOfferPayload::into_transaction_advanced_commerce_offer,
571            ),
572            price: self.price,
573        }
574    }
575}
576
577#[derive(Debug, Deserialize)]
578struct TransactionAdvancedCommerceOfferPayload {
579    price: String,
580    period: SubscriptionPeriodPayload,
581    #[serde(rename = "periodCount")]
582    period_count: i64,
583    reason: String,
584}
585
586impl TransactionAdvancedCommerceOfferPayload {
587    fn into_transaction_advanced_commerce_offer(self) -> TransactionAdvancedCommerceOffer {
588        TransactionAdvancedCommerceOffer {
589            price: self.price,
590            period: self.period.into_subscription_period(),
591            period_count: self.period_count,
592            reason: TransactionAdvancedCommerceOfferReason::from_raw(self.reason),
593        }
594    }
595}
596
597#[derive(Debug, Deserialize)]
598struct TransactionAdvancedCommerceRefundPayload {
599    reason: String,
600    #[serde(rename = "type")]
601    refund_type: String,
602    date: String,
603    amount: String,
604}
605
606impl TransactionAdvancedCommerceRefundPayload {
607    fn into_transaction_advanced_commerce_refund(self) -> TransactionAdvancedCommerceRefund {
608        TransactionAdvancedCommerceRefund {
609            reason: TransactionAdvancedCommerceRefundReason::from_raw(self.reason),
610            refund_type: TransactionAdvancedCommerceRefundType::from_raw(self.refund_type),
611            date: self.date,
612            amount: self.amount,
613        }
614    }
615}
616
617#[derive(Debug, Deserialize)]
618struct RenewalSignedPayload {
619    #[serde(rename = "advancedCommerceInfo")]
620    advanced_commerce_info: Option<RenewalInfoAdvancedCommerceInfoPayload>,
621}
622
623#[derive(Debug, Deserialize)]
624struct RenewalInfoAdvancedCommerceInfoPayload {
625    #[serde(rename = "consistencyToken")]
626    consistency_token: String,
627    #[serde(rename = "requestReferenceID")]
628    request_reference_id: String,
629    #[serde(rename = "taxCode")]
630    tax_code: String,
631    description: String,
632    #[serde(rename = "displayName")]
633    display_name: String,
634    period: SubscriptionPeriodPayload,
635    items: Vec<RenewalInfoAdvancedCommerceItemPayload>,
636}
637
638impl RenewalInfoAdvancedCommerceInfoPayload {
639    fn into_renewal_info_advanced_commerce_info(self) -> RenewalInfoAdvancedCommerceInfo {
640        RenewalInfoAdvancedCommerceInfo {
641            consistency_token: self.consistency_token,
642            request_reference_id: self.request_reference_id,
643            tax_code: self.tax_code,
644            description: self.description,
645            display_name: self.display_name,
646            period: self.period.into_subscription_period(),
647            items: self
648                .items
649                .into_iter()
650                .map(RenewalInfoAdvancedCommerceItemPayload::into_renewal_info_advanced_commerce_item)
651                .collect(),
652        }
653    }
654}
655
656#[derive(Debug, Deserialize)]
657struct RenewalInfoAdvancedCommerceItemPayload {
658    details: TransactionAdvancedCommerceItemDetailsPayload,
659    #[serde(rename = "priceIncreaseInfo")]
660    price_increase_info: Option<RenewalInfoAdvancedCommercePriceIncreaseInfoPayload>,
661}
662
663impl RenewalInfoAdvancedCommerceItemPayload {
664    fn into_renewal_info_advanced_commerce_item(self) -> RenewalInfoAdvancedCommerceItem {
665        RenewalInfoAdvancedCommerceItem {
666            details: self.details.into_transaction_advanced_commerce_item_details(),
667            price_increase_info: self.price_increase_info.map(
668                RenewalInfoAdvancedCommercePriceIncreaseInfoPayload::into_renewal_info_advanced_commerce_price_increase_info,
669            ),
670        }
671    }
672}
673
674#[derive(Debug, Deserialize)]
675struct RenewalInfoAdvancedCommercePriceIncreaseInfoPayload {
676    status: String,
677    price: String,
678    #[serde(rename = "dependentSKUs")]
679    dependent_skus: Vec<String>,
680}
681
682impl RenewalInfoAdvancedCommercePriceIncreaseInfoPayload {
683    fn into_renewal_info_advanced_commerce_price_increase_info(
684        self,
685    ) -> RenewalInfoAdvancedCommercePriceIncreaseInfo {
686        RenewalInfoAdvancedCommercePriceIncreaseInfo {
687            status: RenewalInfoAdvancedCommercePriceIncreaseStatus::from_raw(self.status),
688            price: self.price,
689            dependent_skus: self.dependent_skus,
690        }
691    }
692}
693
694#[derive(Debug, Deserialize)]
695struct AdvancedCommerceProductPayload {
696    id: String,
697    #[serde(rename = "type")]
698    product_type: String,
699}
700
701impl AdvancedCommerceProductPayload {
702    fn into_product(self) -> AdvancedCommerceProduct {
703        AdvancedCommerceProduct {
704            id: self.id,
705            product_type: ProductType::from_raw(self.product_type),
706        }
707    }
708}
709
710#[derive(Debug, Deserialize)]
711#[allow(clippy::unsafe_derive_deserialize)]
712pub(crate) struct AppStoreMerchandisingPresentationResultPayload {
713    kind: String,
714    #[serde(rename = "purchaseResult")]
715    purchase_result: Option<PurchaseResultPayload>,
716}
717
718impl AppStoreMerchandisingPresentationResultPayload {
719    pub(crate) fn into_result(
720        self,
721        transaction_handle: *mut c_void,
722    ) -> Result<AppStoreMerchandisingPresentationResult, StoreKitError> {
723        match self.kind.as_str() {
724            "dismissed" => {
725                if !transaction_handle.is_null() {
726                    unsafe { ffi::sk_transaction_release(transaction_handle) };
727                }
728                Ok(AppStoreMerchandisingPresentationResult::Dismissed)
729            }
730            "purchaseCompleted" => {
731                let purchase_result = self.purchase_result.ok_or_else(|| {
732                    StoreKitError::Unknown(
733                        "App Store merchandising reported a purchase completion without a purchase result"
734                            .to_owned(),
735                    )
736                })?;
737                Ok(AppStoreMerchandisingPresentationResult::PurchaseCompleted(
738                    purchase_result.into_purchase_result(transaction_handle)?,
739                ))
740            }
741            other => {
742                if !transaction_handle.is_null() {
743                    unsafe { ffi::sk_transaction_release(transaction_handle) };
744                }
745                Err(StoreKitError::Unknown(format!(
746                    "unknown App Store merchandising presentation result kind '{other}'"
747                )))
748            }
749        }
750    }
751}
752
753#[derive(Debug, Deserialize)]
754struct TransactionSignedPayload {
755    #[serde(rename = "advancedCommerceInfo")]
756    advanced_commerce_info: Option<TransactionAdvancedCommerceInfoPayload>,
757}
758
759fn parse_transaction_advanced_commerce_info_payload(
760    payload_data: &[u8],
761) -> Result<Option<TransactionAdvancedCommerceInfo>, StoreKitError> {
762    let payload =
763        serde_json::from_slice::<TransactionSignedPayload>(payload_data).map_err(|error| {
764            StoreKitError::InvalidArgument(format!(
765                "failed to parse signed transaction payload JSON: {error}"
766            ))
767        })?;
768    Ok(payload
769        .advanced_commerce_info
770        .map(TransactionAdvancedCommerceInfoPayload::into_transaction_advanced_commerce_info))
771}
772
773fn parse_renewal_advanced_commerce_info_payload(
774    payload_data: &[u8],
775) -> Result<Option<RenewalInfoAdvancedCommerceInfo>, StoreKitError> {
776    let payload =
777        serde_json::from_slice::<RenewalSignedPayload>(payload_data).map_err(|error| {
778            StoreKitError::InvalidArgument(format!(
779                "failed to parse signed renewal payload JSON: {error}"
780            ))
781        })?;
782    Ok(payload
783        .advanced_commerce_info
784        .map(RenewalInfoAdvancedCommerceInfoPayload::into_renewal_info_advanced_commerce_info))
785}