Skip to main content

storekit/
subscription_info.rs

1use core::ptr;
2
3use serde::{Deserialize, Serialize, Serializer};
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.BillingPlanType`.
20pub enum BillingPlanType {
21    /// Represents the `Monthly` `StoreKit` case.
22    Monthly,
23    /// Represents the `UpFront` `StoreKit` case.
24    UpFront,
25    /// Preserves an unrecognized `StoreKit` case.
26    Unknown(String),
27}
28
29impl BillingPlanType {
30    /// Returns the raw `StoreKit` string for this billing plan type.
31    pub fn as_str(&self) -> &str {
32        match self {
33            Self::Monthly => "monthly",
34            Self::UpFront => "upFront",
35            Self::Unknown(value) => value.as_str(),
36        }
37    }
38
39    pub(crate) fn from_raw(raw: String) -> Self {
40        match raw.as_str() {
41            "monthly" => Self::Monthly,
42            "upFront" => Self::UpFront,
43            _ => Self::Unknown(raw),
44        }
45    }
46}
47
48impl Serialize for BillingPlanType {
49    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50    where
51        S: Serializer,
52    {
53        serializer.serialize_str(self.as_str())
54    }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58/// Wraps `StoreKit.Product.SubscriptionInfo.CommitmentInfo`.
59pub struct SubscriptionCommitmentInfo {
60    /// Price reported by `StoreKit`.
61    pub price: String,
62    /// Localized display price reported by `StoreKit`.
63    pub display_price: String,
64    /// Billing period reported by `StoreKit`.
65    pub period: SubscriptionPeriod,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69/// Wraps `StoreKit.Product.SubscriptionInfo.PricingTerms`.
70pub struct SubscriptionPricingTerms {
71    /// Billing price reported by `StoreKit`.
72    pub billing_price: String,
73    /// Localized billing display price reported by `StoreKit`.
74    pub billing_display_price: String,
75    /// Billing period reported by `StoreKit`.
76    pub billing_period: SubscriptionPeriod,
77    /// Billing plan type reported by `StoreKit`.
78    pub billing_plan_type: BillingPlanType,
79    /// Commitment info reported by `StoreKit`.
80    pub commitment_info: SubscriptionCommitmentInfo,
81    /// Subscription offers reported by `StoreKit`.
82    pub subscription_offers: Vec<SubscriptionOffer>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86/// Wraps `StoreKit.Product.SubscriptionInfo`.
87pub struct SubscriptionInfo {
88    /// Introductory offer reported by `StoreKit`.
89    pub introductory_offer: Option<SubscriptionOffer>,
90    /// Promotional offers reported by `StoreKit`.
91    pub promotional_offers: Vec<SubscriptionOffer>,
92    /// Win-back offers reported by `StoreKit`.
93    pub win_back_offers: Vec<SubscriptionOffer>,
94    /// Subscription group identifier reported by `StoreKit`.
95    pub subscription_group_id: String,
96    /// Subscription period reported by `StoreKit`.
97    pub subscription_period: SubscriptionPeriod,
98    /// Pricing terms reported by `StoreKit`.
99    pub pricing_terms: Vec<SubscriptionPricingTerms>,
100    /// Subscription group level reported by `StoreKit`.
101    pub group_level: Option<i64>,
102    /// Subscription group display name reported by `StoreKit`.
103    pub group_display_name: Option<String>,
104}
105
106impl SubscriptionInfo {
107    /// Returns whether `StoreKit` reports that this subscription group is eligible for an introductory offer.
108    pub fn is_eligible_for_intro_offer(&self) -> Result<bool, StoreKitError> {
109        Self::is_eligible_for_intro_offer_for(&self.subscription_group_id)
110    }
111
112    /// Returns whether `StoreKit` reports that the supplied subscription group is eligible for an introductory offer.
113    pub fn is_eligible_for_intro_offer_for(group_id: &str) -> Result<bool, StoreKitError> {
114        let group_id = cstring_from_str(group_id, "subscription group id")?;
115        let mut raw_value = 0;
116        let mut error_message = ptr::null_mut();
117        let status = unsafe {
118            ffi::sk_subscription_info_is_eligible_for_intro_offer(
119                group_id.as_ptr(),
120                &mut raw_value,
121                &mut error_message,
122            )
123        };
124        if status == ffi::status::OK {
125            Ok(raw_value != 0)
126        } else {
127            Err(unsafe { error_from_status(status, error_message) })
128        }
129    }
130
131    /// Fetches the `StoreKit` subscription statuses for this subscription group.
132    pub fn status(&self) -> Result<Vec<SubscriptionStatus>, StoreKitError> {
133        Self::status_for(&self.subscription_group_id)
134    }
135
136    /// Fetches the `StoreKit` subscription statuses for the supplied subscription group identifier.
137    pub fn status_for(group_id: &str) -> Result<Vec<SubscriptionStatus>, StoreKitError> {
138        let group_id = cstring_from_str(group_id, "subscription group id")?;
139        let mut statuses_json = ptr::null_mut();
140        let mut error_message = ptr::null_mut();
141        let status = unsafe {
142            ffi::sk_subscription_info_statuses_json(
143                group_id.as_ptr(),
144                &mut statuses_json,
145                &mut error_message,
146            )
147        };
148        if status != ffi::status::OK {
149            return Err(unsafe { error_from_status(status, error_message) });
150        }
151        let payloads = unsafe {
152            parse_json_ptr::<Vec<SubscriptionStatusPayload>>(statuses_json, "subscription statuses")
153        }?;
154        payloads
155            .into_iter()
156            .map(SubscriptionStatusPayload::into_subscription_status)
157            .collect::<Result<Vec<_>, _>>()
158    }
159
160    /// Fetches the `StoreKit` subscription status for the supplied transaction identifier.
161    pub fn status_for_transaction(
162        transaction_id: u64,
163    ) -> Result<Option<SubscriptionStatus>, StoreKitError> {
164        let transaction_id = cstring_from_str(&transaction_id.to_string(), "transaction id")?;
165        let mut status_json = ptr::null_mut();
166        let mut error_message = ptr::null_mut();
167        let status = unsafe {
168            ffi::sk_subscription_info_status_for_transaction(
169                transaction_id.as_ptr(),
170                &mut status_json,
171                &mut error_message,
172            )
173        };
174        if status != ffi::status::OK {
175            return Err(unsafe { error_from_status(status, error_message) });
176        }
177        unsafe {
178            parse_optional_json_ptr::<SubscriptionStatusPayload>(
179                status_json,
180                "subscription status for transaction",
181            )
182        }
183        .and_then(|payload| {
184            payload
185                .map(SubscriptionStatusPayload::into_subscription_status)
186                .transpose()
187        })
188    }
189}
190
191#[derive(Debug, Clone)]
192/// Wraps `StoreKit.Product.SubscriptionInfo.Status`.
193pub struct SubscriptionStatus {
194    /// State reported by `StoreKit`.
195    pub state: RenewalState,
196    /// Transaction payload returned by `StoreKit`.
197    pub transaction: VerificationResult<Transaction>,
198    /// Renewal info payload returned by `StoreKit`.
199    pub renewal_info: VerificationResult<RenewalInfo>,
200}
201
202#[derive(Debug, Deserialize)]
203pub(crate) struct SubscriptionInfoPayload {
204    #[serde(rename = "introductoryOffer")]
205    introductory_offer: Option<SubscriptionOfferPayload>,
206    #[serde(rename = "promotionalOffers")]
207    promotional_offers: Vec<SubscriptionOfferPayload>,
208    #[serde(rename = "winBackOffers")]
209    win_back_offers: Vec<SubscriptionOfferPayload>,
210    #[serde(rename = "subscriptionGroupID")]
211    subscription_group_id: String,
212    #[serde(rename = "subscriptionPeriod")]
213    subscription_period: SubscriptionPeriodPayload,
214    #[serde(default, rename = "pricingTerms")]
215    pricing_terms: Vec<SubscriptionPricingTermsPayload>,
216    #[serde(rename = "groupLevel")]
217    group_level: Option<i64>,
218    #[serde(rename = "groupDisplayName")]
219    group_display_name: Option<String>,
220}
221
222#[derive(Debug, Deserialize)]
223pub(crate) struct SubscriptionCommitmentInfoPayload {
224    price: String,
225    #[serde(rename = "displayPrice")]
226    display_price: String,
227    period: SubscriptionPeriodPayload,
228}
229
230impl SubscriptionCommitmentInfoPayload {
231    pub(crate) fn into_subscription_commitment_info(self) -> SubscriptionCommitmentInfo {
232        SubscriptionCommitmentInfo {
233            price: self.price,
234            display_price: self.display_price,
235            period: self.period.into_subscription_period(),
236        }
237    }
238}
239
240#[derive(Debug, Deserialize)]
241pub(crate) struct SubscriptionPricingTermsPayload {
242    #[serde(rename = "billingPrice")]
243    billing_price: String,
244    #[serde(rename = "billingDisplayPrice")]
245    billing_display_price: String,
246    #[serde(rename = "billingPeriod")]
247    billing_period: SubscriptionPeriodPayload,
248    #[serde(rename = "billingPlanType")]
249    billing_plan_type: String,
250    #[serde(rename = "commitmentInfo")]
251    commitment_info: SubscriptionCommitmentInfoPayload,
252    #[serde(default, rename = "subscriptionOffers")]
253    subscription_offers: Vec<SubscriptionOfferPayload>,
254}
255
256impl SubscriptionPricingTermsPayload {
257    pub(crate) fn into_subscription_pricing_terms(self) -> SubscriptionPricingTerms {
258        SubscriptionPricingTerms {
259            billing_price: self.billing_price,
260            billing_display_price: self.billing_display_price,
261            billing_period: self.billing_period.into_subscription_period(),
262            billing_plan_type: BillingPlanType::from_raw(self.billing_plan_type),
263            commitment_info: self.commitment_info.into_subscription_commitment_info(),
264            subscription_offers: self
265                .subscription_offers
266                .into_iter()
267                .map(SubscriptionOfferPayload::into_subscription_offer)
268                .collect(),
269        }
270    }
271}
272
273impl SubscriptionInfoPayload {
274    pub(crate) fn into_subscription_info(self) -> SubscriptionInfo {
275        SubscriptionInfo {
276            introductory_offer: self
277                .introductory_offer
278                .map(SubscriptionOfferPayload::into_subscription_offer),
279            promotional_offers: self
280                .promotional_offers
281                .into_iter()
282                .map(SubscriptionOfferPayload::into_subscription_offer)
283                .collect(),
284            win_back_offers: self
285                .win_back_offers
286                .into_iter()
287                .map(SubscriptionOfferPayload::into_subscription_offer)
288                .collect(),
289            subscription_group_id: self.subscription_group_id,
290            subscription_period: self.subscription_period.into_subscription_period(),
291            pricing_terms: self
292                .pricing_terms
293                .into_iter()
294                .map(SubscriptionPricingTermsPayload::into_subscription_pricing_terms)
295                .collect(),
296            group_level: self.group_level,
297            group_display_name: self.group_display_name,
298        }
299    }
300}
301
302#[derive(Debug, Deserialize)]
303pub(crate) struct SubscriptionStatusPayload {
304    state: String,
305    transaction: VerificationResultPayload<TransactionPayload>,
306    #[serde(rename = "renewalInfo")]
307    renewal_info: VerificationResultPayload<RenewalInfoPayload>,
308}
309
310impl SubscriptionStatusPayload {
311    pub(crate) fn into_subscription_status(self) -> Result<SubscriptionStatus, StoreKitError> {
312        Ok(SubscriptionStatus {
313            state: RenewalState::from_raw(self.state),
314            transaction: self
315                .transaction
316                .into_result(Transaction::from_snapshot_payload)?,
317            renewal_info: self
318                .renewal_info
319                .into_result(|payload| Ok(payload.into_renewal_info()))?,
320        })
321    }
322}