Skip to main content

storekit/
subscription_info.rs

1use core::ptr;
2
3use serde::Deserialize;
4
5use crate::error::StoreKitError;
6use crate::ffi;
7use crate::private::{
8    cstring_from_str, error_from_status, parse_json_ptr, parse_optional_json_ptr,
9};
10use crate::renewal_info::{RenewalInfo, RenewalInfoPayload};
11use crate::renewal_state::RenewalState;
12use crate::subscription::{
13    SubscriptionOffer, SubscriptionOfferPayload, SubscriptionPeriod, SubscriptionPeriodPayload,
14};
15use crate::transaction::{Transaction, TransactionPayload};
16use crate::verification_result::{VerificationResult, VerificationResultPayload};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19/// Wraps `StoreKit.Product.SubscriptionInfo`.
20pub struct SubscriptionInfo {
21    /// Introductory offer reported by `StoreKit`.
22    pub introductory_offer: Option<SubscriptionOffer>,
23    /// Promotional offers reported by `StoreKit`.
24    pub promotional_offers: Vec<SubscriptionOffer>,
25    /// Win-back offers reported by `StoreKit`.
26    pub win_back_offers: Vec<SubscriptionOffer>,
27    /// Subscription group identifier reported by `StoreKit`.
28    pub subscription_group_id: String,
29    /// Subscription period reported by `StoreKit`.
30    pub subscription_period: SubscriptionPeriod,
31    /// Subscription group level reported by `StoreKit`.
32    pub group_level: Option<i64>,
33    /// Subscription group display name reported by `StoreKit`.
34    pub group_display_name: Option<String>,
35}
36
37impl SubscriptionInfo {
38    /// Returns whether `StoreKit` reports that this subscription group is eligible for an introductory offer.
39    pub fn is_eligible_for_intro_offer(&self) -> Result<bool, StoreKitError> {
40        Self::is_eligible_for_intro_offer_for(&self.subscription_group_id)
41    }
42
43    /// Returns whether `StoreKit` reports that the supplied subscription group is eligible for an introductory offer.
44    pub fn is_eligible_for_intro_offer_for(group_id: &str) -> Result<bool, StoreKitError> {
45        let group_id = cstring_from_str(group_id, "subscription group id")?;
46        let mut raw_value = 0;
47        let mut error_message = ptr::null_mut();
48        let status = unsafe {
49            ffi::sk_subscription_info_is_eligible_for_intro_offer(
50                group_id.as_ptr(),
51                &mut raw_value,
52                &mut error_message,
53            )
54        };
55        if status == ffi::status::OK {
56            Ok(raw_value != 0)
57        } else {
58            Err(unsafe { error_from_status(status, error_message) })
59        }
60    }
61
62    /// Fetches the `StoreKit` subscription statuses for this subscription group.
63    pub fn status(&self) -> Result<Vec<SubscriptionStatus>, StoreKitError> {
64        Self::status_for(&self.subscription_group_id)
65    }
66
67    /// Fetches the `StoreKit` subscription statuses for the supplied subscription group identifier.
68    pub fn status_for(group_id: &str) -> Result<Vec<SubscriptionStatus>, StoreKitError> {
69        let group_id = cstring_from_str(group_id, "subscription group id")?;
70        let mut statuses_json = ptr::null_mut();
71        let mut error_message = ptr::null_mut();
72        let status = unsafe {
73            ffi::sk_subscription_info_statuses_json(
74                group_id.as_ptr(),
75                &mut statuses_json,
76                &mut error_message,
77            )
78        };
79        if status != ffi::status::OK {
80            return Err(unsafe { error_from_status(status, error_message) });
81        }
82        let payloads = unsafe {
83            parse_json_ptr::<Vec<SubscriptionStatusPayload>>(statuses_json, "subscription statuses")
84        }?;
85        payloads
86            .into_iter()
87            .map(SubscriptionStatusPayload::into_subscription_status)
88            .collect::<Result<Vec<_>, _>>()
89    }
90
91    /// Fetches the `StoreKit` subscription status for the supplied transaction identifier.
92    pub fn status_for_transaction(
93        transaction_id: u64,
94    ) -> Result<Option<SubscriptionStatus>, StoreKitError> {
95        let transaction_id = cstring_from_str(&transaction_id.to_string(), "transaction id")?;
96        let mut status_json = ptr::null_mut();
97        let mut error_message = ptr::null_mut();
98        let status = unsafe {
99            ffi::sk_subscription_info_status_for_transaction(
100                transaction_id.as_ptr(),
101                &mut status_json,
102                &mut error_message,
103            )
104        };
105        if status != ffi::status::OK {
106            return Err(unsafe { error_from_status(status, error_message) });
107        }
108        unsafe {
109            parse_optional_json_ptr::<SubscriptionStatusPayload>(
110                status_json,
111                "subscription status for transaction",
112            )
113        }
114        .and_then(|payload| {
115            payload
116                .map(SubscriptionStatusPayload::into_subscription_status)
117                .transpose()
118        })
119    }
120}
121
122#[derive(Debug, Clone)]
123/// Wraps `StoreKit.Product.SubscriptionInfo.Status`.
124pub struct SubscriptionStatus {
125    /// State reported by `StoreKit`.
126    pub state: RenewalState,
127    /// Transaction payload returned by `StoreKit`.
128    pub transaction: VerificationResult<Transaction>,
129    /// Renewal info payload returned by `StoreKit`.
130    pub renewal_info: VerificationResult<RenewalInfo>,
131}
132
133#[derive(Debug, Deserialize)]
134pub(crate) struct SubscriptionInfoPayload {
135    #[serde(rename = "introductoryOffer")]
136    introductory_offer: Option<SubscriptionOfferPayload>,
137    #[serde(rename = "promotionalOffers")]
138    promotional_offers: Vec<SubscriptionOfferPayload>,
139    #[serde(rename = "winBackOffers")]
140    win_back_offers: Vec<SubscriptionOfferPayload>,
141    #[serde(rename = "subscriptionGroupID")]
142    subscription_group_id: String,
143    #[serde(rename = "subscriptionPeriod")]
144    subscription_period: SubscriptionPeriodPayload,
145    #[serde(rename = "groupLevel")]
146    group_level: Option<i64>,
147    #[serde(rename = "groupDisplayName")]
148    group_display_name: Option<String>,
149}
150
151impl SubscriptionInfoPayload {
152    pub(crate) fn into_subscription_info(self) -> SubscriptionInfo {
153        SubscriptionInfo {
154            introductory_offer: self
155                .introductory_offer
156                .map(SubscriptionOfferPayload::into_subscription_offer),
157            promotional_offers: self
158                .promotional_offers
159                .into_iter()
160                .map(SubscriptionOfferPayload::into_subscription_offer)
161                .collect(),
162            win_back_offers: self
163                .win_back_offers
164                .into_iter()
165                .map(SubscriptionOfferPayload::into_subscription_offer)
166                .collect(),
167            subscription_group_id: self.subscription_group_id,
168            subscription_period: self.subscription_period.into_subscription_period(),
169            group_level: self.group_level,
170            group_display_name: self.group_display_name,
171        }
172    }
173}
174
175#[derive(Debug, Deserialize)]
176pub(crate) struct SubscriptionStatusPayload {
177    state: String,
178    transaction: VerificationResultPayload<TransactionPayload>,
179    #[serde(rename = "renewalInfo")]
180    renewal_info: VerificationResultPayload<RenewalInfoPayload>,
181}
182
183impl SubscriptionStatusPayload {
184    pub(crate) fn into_subscription_status(self) -> Result<SubscriptionStatus, StoreKitError> {
185        Ok(SubscriptionStatus {
186            state: RenewalState::from_raw(self.state),
187            transaction: self
188                .transaction
189                .into_result(Transaction::from_snapshot_payload)?,
190            renewal_info: self
191                .renewal_info
192                .into_result(|payload| Ok(payload.into_renewal_info()))?,
193        })
194    }
195}