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