Skip to main content

storekit/
error.rs

1use core::ffi::c_char;
2use std::collections::HashMap;
3use std::fmt;
4use std::sync::{Mutex, OnceLock};
5
6use serde::Deserialize;
7
8use crate::ffi;
9use crate::private::take_string;
10
11#[derive(Debug, Clone)]
12pub enum StoreKitError {
13    InvalidArgument(String),
14    TimedOut(String),
15    NotSupported(String),
16    Framework(StoreKitFrameworkError),
17    Verification(VerificationFailure),
18    Unknown(String),
19}
20
21impl StoreKitError {
22    pub fn typed(&self) -> Option<TypedStoreKitError> {
23        match self {
24            Self::Framework(error) => lookup_typed_framework_error(error),
25            _ => None,
26        }
27    }
28
29    pub fn storekit_api_error(&self) -> Option<StoreKitApiError> {
30        match self.typed() {
31            Some(TypedStoreKitError::StoreKit(error)) => Some(error),
32            _ => None,
33        }
34    }
35
36    pub fn product_purchase_error(&self) -> Option<ProductPurchaseError> {
37        match self.typed() {
38            Some(TypedStoreKitError::Purchase(error)) => Some(error),
39            _ => None,
40        }
41    }
42
43    pub fn refund_request_error(&self) -> Option<RefundRequestError> {
44        match self.typed() {
45            Some(TypedStoreKitError::RefundRequest(error)) => Some(error),
46            _ => None,
47        }
48    }
49
50    pub fn invalid_request_error(&self) -> Option<InvalidRequestError> {
51        match self.typed() {
52            Some(TypedStoreKitError::InvalidRequest(error)) => Some(error),
53            _ => None,
54        }
55    }
56}
57
58impl fmt::Display for StoreKitError {
59    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::InvalidArgument(message)
62            | Self::TimedOut(message)
63            | Self::NotSupported(message)
64            | Self::Unknown(message) => formatter.write_str(message),
65            Self::Framework(error) => write!(
66                formatter,
67                "{} (domain={}, code={})",
68                error.localized_description, error.domain, error.code
69            ),
70            Self::Verification(error) => write!(
71                formatter,
72                "{} ({})",
73                error.localized_description,
74                error.code.as_str()
75            ),
76        }
77    }
78}
79
80impl std::error::Error for StoreKitError {}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct StoreKitFrameworkError {
84    pub domain: String,
85    pub code: i64,
86    pub localized_description: String,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum TypedStoreKitError {
91    StoreKit(StoreKitApiError),
92    Purchase(ProductPurchaseError),
93    RefundRequest(RefundRequestError),
94    InvalidRequest(InvalidRequestError),
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct StoreKitApiError {
99    pub code: StoreKitApiErrorCode,
100    pub error_description: Option<String>,
101    pub failure_reason: Option<String>,
102    pub recovery_suggestion: Option<String>,
103    pub underlying_domain: Option<String>,
104    pub underlying_code: Option<i64>,
105    pub underlying_description: Option<String>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum StoreKitApiErrorCode {
110    Unknown,
111    UserCancelled,
112    NetworkError,
113    SystemError,
114    NotAvailableInStorefront,
115    NotEntitled,
116    Unsupported,
117    Other(String),
118}
119
120impl StoreKitApiErrorCode {
121    pub fn as_str(&self) -> &str {
122        match self {
123            Self::Unknown => "unknown",
124            Self::UserCancelled => "userCancelled",
125            Self::NetworkError => "networkError",
126            Self::SystemError => "systemError",
127            Self::NotAvailableInStorefront => "notAvailableInStorefront",
128            Self::NotEntitled => "notEntitled",
129            Self::Unsupported => "unsupported",
130            Self::Other(value) => value.as_str(),
131        }
132    }
133
134    fn from_raw(raw: String) -> Self {
135        match raw.as_str() {
136            "unknown" => Self::Unknown,
137            "userCancelled" => Self::UserCancelled,
138            "networkError" => Self::NetworkError,
139            "systemError" => Self::SystemError,
140            "notAvailableInStorefront" => Self::NotAvailableInStorefront,
141            "notEntitled" => Self::NotEntitled,
142            "unsupported" => Self::Unsupported,
143            _ => Self::Other(raw),
144        }
145    }
146
147    const fn numeric_code(&self) -> i64 {
148        match self {
149            Self::Unknown => 0,
150            Self::UserCancelled => 1,
151            Self::NetworkError => 2,
152            Self::SystemError => 3,
153            Self::NotAvailableInStorefront => 4,
154            Self::NotEntitled => 5,
155            Self::Unsupported => 6,
156            Self::Other(_) => -1,
157        }
158    }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct ProductPurchaseError {
163    pub code: ProductPurchaseErrorCode,
164    pub error_description: Option<String>,
165    pub failure_reason: Option<String>,
166    pub recovery_suggestion: Option<String>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub enum ProductPurchaseErrorCode {
171    InvalidQuantity,
172    ProductUnavailable,
173    PurchaseNotAllowed,
174    IneligibleForOffer,
175    InvalidOfferIdentifier,
176    InvalidOfferPrice,
177    InvalidOfferSignature,
178    MissingOfferParameters,
179    Other(String),
180}
181
182impl ProductPurchaseErrorCode {
183    pub fn as_str(&self) -> &str {
184        match self {
185            Self::InvalidQuantity => "invalidQuantity",
186            Self::ProductUnavailable => "productUnavailable",
187            Self::PurchaseNotAllowed => "purchaseNotAllowed",
188            Self::IneligibleForOffer => "ineligibleForOffer",
189            Self::InvalidOfferIdentifier => "invalidOfferIdentifier",
190            Self::InvalidOfferPrice => "invalidOfferPrice",
191            Self::InvalidOfferSignature => "invalidOfferSignature",
192            Self::MissingOfferParameters => "missingOfferParameters",
193            Self::Other(value) => value.as_str(),
194        }
195    }
196
197    fn from_raw(raw: String) -> Self {
198        match raw.as_str() {
199            "invalidQuantity" => Self::InvalidQuantity,
200            "productUnavailable" => Self::ProductUnavailable,
201            "purchaseNotAllowed" => Self::PurchaseNotAllowed,
202            "ineligibleForOffer" => Self::IneligibleForOffer,
203            "invalidOfferIdentifier" => Self::InvalidOfferIdentifier,
204            "invalidOfferPrice" => Self::InvalidOfferPrice,
205            "invalidOfferSignature" => Self::InvalidOfferSignature,
206            "missingOfferParameters" => Self::MissingOfferParameters,
207            _ => Self::Other(raw),
208        }
209    }
210
211    const fn numeric_code(&self) -> i64 {
212        match self {
213            Self::InvalidQuantity => 0,
214            Self::ProductUnavailable => 1,
215            Self::PurchaseNotAllowed => 2,
216            Self::IneligibleForOffer => 3,
217            Self::InvalidOfferIdentifier => 4,
218            Self::InvalidOfferPrice => 5,
219            Self::InvalidOfferSignature => 6,
220            Self::MissingOfferParameters => 7,
221            Self::Other(_) => -1,
222        }
223    }
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub struct RefundRequestError {
228    pub code: RefundRequestErrorCode,
229    pub error_description: Option<String>,
230    pub failure_reason: Option<String>,
231    pub recovery_suggestion: Option<String>,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum RefundRequestErrorCode {
236    DuplicateRequest,
237    Failed,
238    Other(String),
239}
240
241impl RefundRequestErrorCode {
242    pub fn as_str(&self) -> &str {
243        match self {
244            Self::DuplicateRequest => "duplicateRequest",
245            Self::Failed => "failed",
246            Self::Other(value) => value.as_str(),
247        }
248    }
249
250    fn from_raw(raw: String) -> Self {
251        match raw.as_str() {
252            "duplicateRequest" => Self::DuplicateRequest,
253            "failed" => Self::Failed,
254            _ => Self::Other(raw),
255        }
256    }
257
258    const fn numeric_code(&self) -> i64 {
259        match self {
260            Self::DuplicateRequest => 0,
261            Self::Failed => 1,
262            Self::Other(_) => -1,
263        }
264    }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct InvalidRequestError {
269    pub code: i64,
270    pub message: String,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct VerificationFailure {
275    pub code: VerificationErrorCode,
276    pub localized_description: String,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub enum VerificationErrorCode {
281    RevokedCertificate,
282    InvalidCertificateChain,
283    InvalidDeviceVerification,
284    InvalidEncoding,
285    InvalidSignature,
286    MissingRequiredProperties,
287    Unknown(String),
288}
289
290impl VerificationErrorCode {
291    pub fn as_str(&self) -> &str {
292        match self {
293            Self::RevokedCertificate => "revokedCertificate",
294            Self::InvalidCertificateChain => "invalidCertificateChain",
295            Self::InvalidDeviceVerification => "invalidDeviceVerification",
296            Self::InvalidEncoding => "invalidEncoding",
297            Self::InvalidSignature => "invalidSignature",
298            Self::MissingRequiredProperties => "missingRequiredProperties",
299            Self::Unknown(value) => value.as_str(),
300        }
301    }
302
303    pub(crate) fn from_raw(raw: String) -> Self {
304        match raw.as_str() {
305            "revokedCertificate" => Self::RevokedCertificate,
306            "invalidCertificateChain" => Self::InvalidCertificateChain,
307            "invalidDeviceVerification" => Self::InvalidDeviceVerification,
308            "invalidEncoding" => Self::InvalidEncoding,
309            "invalidSignature" => Self::InvalidSignature,
310            "missingRequiredProperties" => Self::MissingRequiredProperties,
311            _ => Self::Unknown(raw),
312        }
313    }
314}
315
316#[derive(Debug, Deserialize)]
317pub(crate) struct VerificationErrorPayload {
318    #[allow(dead_code)]
319    kind: String,
320    code: String,
321    #[serde(rename = "localizedDescription")]
322    localized_description: String,
323}
324
325impl VerificationFailure {
326    pub(crate) fn from_payload(payload: VerificationErrorPayload) -> Self {
327        Self {
328            code: VerificationErrorCode::from_raw(payload.code),
329            localized_description: payload.localized_description,
330        }
331    }
332}
333
334#[derive(Debug, Deserialize)]
335#[serde(tag = "kind")]
336enum FrameworkErrorPayload {
337    #[serde(rename = "framework")]
338    Framework {
339        domain: String,
340        code: i64,
341        #[serde(rename = "localizedDescription")]
342        localized_description: String,
343    },
344    #[serde(rename = "storekitError")]
345    StoreKit {
346        code: String,
347        #[serde(rename = "errorDescription")]
348        error_description: Option<String>,
349        #[serde(rename = "failureReason")]
350        failure_reason: Option<String>,
351        #[serde(rename = "recoverySuggestion")]
352        recovery_suggestion: Option<String>,
353        #[serde(rename = "underlyingDomain")]
354        underlying_domain: Option<String>,
355        #[serde(rename = "underlyingCode")]
356        underlying_code: Option<i64>,
357        #[serde(rename = "underlyingDescription")]
358        underlying_description: Option<String>,
359    },
360    #[serde(rename = "purchaseError")]
361    Purchase {
362        code: String,
363        #[serde(rename = "errorDescription")]
364        error_description: Option<String>,
365        #[serde(rename = "failureReason")]
366        failure_reason: Option<String>,
367        #[serde(rename = "recoverySuggestion")]
368        recovery_suggestion: Option<String>,
369    },
370    #[serde(rename = "refundRequestError")]
371    RefundRequest {
372        code: String,
373        #[serde(rename = "errorDescription")]
374        error_description: Option<String>,
375        #[serde(rename = "failureReason")]
376        failure_reason: Option<String>,
377        #[serde(rename = "recoverySuggestion")]
378        recovery_suggestion: Option<String>,
379    },
380    #[serde(rename = "invalidRequestError")]
381    InvalidRequest {
382        code: i64,
383        message: String,
384    },
385}
386
387impl FrameworkErrorPayload {
388    fn into_storekit_error(self) -> StoreKitError {
389        match self {
390            Self::Framework {
391                domain,
392                code,
393                localized_description,
394            } => StoreKitError::Framework(StoreKitFrameworkError {
395                domain,
396                code,
397                localized_description,
398            }),
399            Self::StoreKit {
400                code,
401                error_description,
402                failure_reason,
403                recovery_suggestion,
404                underlying_domain,
405                underlying_code,
406                underlying_description,
407            } => typed_framework_error(
408                "StoreKit.StoreKitError",
409                StoreKitApiErrorCode::from_raw(code.clone()).numeric_code(),
410                error_description
411                    .clone()
412                    .unwrap_or_else(|| StoreKitApiErrorCode::from_raw(code.clone()).as_str().to_owned()),
413                TypedStoreKitError::StoreKit(StoreKitApiError {
414                    code: StoreKitApiErrorCode::from_raw(code),
415                    error_description,
416                    failure_reason,
417                    recovery_suggestion,
418                    underlying_domain,
419                    underlying_code,
420                    underlying_description,
421                }),
422            ),
423            Self::Purchase {
424                code,
425                error_description,
426                failure_reason,
427                recovery_suggestion,
428            } => typed_framework_error(
429                "StoreKit.Product.PurchaseError",
430                ProductPurchaseErrorCode::from_raw(code.clone()).numeric_code(),
431                error_description.clone().unwrap_or_else(|| {
432                    ProductPurchaseErrorCode::from_raw(code.clone())
433                        .as_str()
434                        .to_owned()
435                }),
436                TypedStoreKitError::Purchase(ProductPurchaseError {
437                    code: ProductPurchaseErrorCode::from_raw(code),
438                    error_description,
439                    failure_reason,
440                    recovery_suggestion,
441                }),
442            ),
443            Self::RefundRequest {
444                code,
445                error_description,
446                failure_reason,
447                recovery_suggestion,
448            } => typed_framework_error(
449                "StoreKit.Transaction.RefundRequestError",
450                RefundRequestErrorCode::from_raw(code.clone()).numeric_code(),
451                error_description.clone().unwrap_or_else(|| {
452                    RefundRequestErrorCode::from_raw(code.clone())
453                        .as_str()
454                        .to_owned()
455                }),
456                TypedStoreKitError::RefundRequest(RefundRequestError {
457                    code: RefundRequestErrorCode::from_raw(code),
458                    error_description,
459                    failure_reason,
460                    recovery_suggestion,
461                }),
462            ),
463            Self::InvalidRequest { code, message } => typed_framework_error(
464                "StoreKit.InvalidRequestError",
465                code,
466                message.clone(),
467                TypedStoreKitError::InvalidRequest(InvalidRequestError { code, message }),
468            ),
469        }
470    }
471}
472
473pub(crate) unsafe fn from_swift(status: i32, err_msg: *mut c_char) -> StoreKitError {
474    let message = take_string(err_msg);
475    match status {
476        ffi::status::INVALID_ARGUMENT => StoreKitError::InvalidArgument(
477            message.unwrap_or_else(|| "StoreKit reported an invalid argument".to_owned()),
478        ),
479        ffi::status::TIMED_OUT => StoreKitError::TimedOut(
480            message.unwrap_or_else(|| "StoreKit operation timed out".to_owned()),
481        ),
482        ffi::status::NOT_SUPPORTED => StoreKitError::NotSupported(
483            message.unwrap_or_else(|| "StoreKit operation is not supported".to_owned()),
484        ),
485        ffi::status::FRAMEWORK_ERROR => parse_framework_error(message),
486        ffi::status::VERIFICATION_ERROR => parse_verification_error(message),
487        _ => StoreKitError::Unknown(
488            message
489                .unwrap_or_else(|| format!("StoreKit bridge returned unexpected status {status}")),
490        ),
491    }
492}
493
494fn parse_framework_error(message: Option<String>) -> StoreKitError {
495    message.map_or_else(
496        || {
497            StoreKitError::Framework(StoreKitFrameworkError {
498                domain: "StoreKit".to_owned(),
499                code: i64::from(ffi::status::FRAMEWORK_ERROR),
500                localized_description: "StoreKit framework error".to_owned(),
501            })
502        },
503        |json| {
504            serde_json::from_str::<FrameworkErrorPayload>(&json).map_or_else(
505                |_| {
506                    StoreKitError::Framework(StoreKitFrameworkError {
507                        domain: "StoreKit".to_owned(),
508                        code: i64::from(ffi::status::FRAMEWORK_ERROR),
509                        localized_description: json,
510                    })
511                },
512                FrameworkErrorPayload::into_storekit_error,
513            )
514        },
515    )
516}
517
518fn parse_verification_error(message: Option<String>) -> StoreKitError {
519    message.map_or_else(
520        || {
521            StoreKitError::Verification(VerificationFailure {
522                code: VerificationErrorCode::Unknown("unknown".to_owned()),
523                localized_description: "StoreKit verification failed".to_owned(),
524            })
525        },
526        |json| {
527            serde_json::from_str::<VerificationErrorPayload>(&json).map_or_else(
528                |_| {
529                    StoreKitError::Verification(VerificationFailure {
530                        code: VerificationErrorCode::Unknown("unknown".to_owned()),
531                        localized_description: json,
532                    })
533                },
534                |payload| StoreKitError::Verification(VerificationFailure::from_payload(payload)),
535            )
536        },
537    )
538}
539
540fn typed_framework_error(
541    domain: &str,
542    code: i64,
543    localized_description: String,
544    typed_error: TypedStoreKitError,
545) -> StoreKitError {
546    let framework_error = StoreKitFrameworkError {
547        domain: domain.to_owned(),
548        code,
549        localized_description,
550    };
551    register_typed_framework_error(&framework_error, typed_error);
552    StoreKitError::Framework(framework_error)
553}
554
555fn register_typed_framework_error(error: &StoreKitFrameworkError, typed_error: TypedStoreKitError) {
556    typed_framework_error_registry()
557        .lock()
558        .expect("typed StoreKit error registry poisoned")
559        .insert(framework_error_key(error), typed_error);
560}
561
562fn lookup_typed_framework_error(error: &StoreKitFrameworkError) -> Option<TypedStoreKitError> {
563    typed_framework_error_registry()
564        .lock()
565        .expect("typed StoreKit error registry poisoned")
566        .get(&framework_error_key(error))
567        .cloned()
568}
569
570fn framework_error_key(error: &StoreKitFrameworkError) -> String {
571    format!("{}\u{0}{}\u{0}", error.domain, error.code) + &error.localized_description
572}
573
574fn typed_framework_error_registry() -> &'static Mutex<HashMap<String, TypedStoreKitError>> {
575    static REGISTRY: OnceLock<Mutex<HashMap<String, TypedStoreKitError>>> = OnceLock::new();
576    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    #[test]
584    fn parses_product_purchase_errors_into_typed_details() {
585        let error = parse_framework_error(Some(
586            r#"{"kind":"purchaseError","code":"invalidQuantity","errorDescription":"bad quantity","failureReason":"quantity must be positive","recoverySuggestion":"choose a quantity greater than zero"}"#
587                .to_owned(),
588        ));
589        let typed = error
590            .product_purchase_error()
591            .expect("typed product purchase error");
592        assert_eq!(typed.code, ProductPurchaseErrorCode::InvalidQuantity);
593        assert_eq!(typed.error_description.as_deref(), Some("bad quantity"));
594    }
595
596    #[test]
597    fn parses_storekit_errors_into_typed_details() {
598        let error = parse_framework_error(Some(
599            r#"{"kind":"storekitError","code":"unsupported","errorDescription":"unsupported","failureReason":"not available here","recoverySuggestion":"try a supported storefront"}"#
600                .to_owned(),
601        ));
602        let typed = error.storekit_api_error().expect("typed StoreKit API error");
603        assert_eq!(typed.code, StoreKitApiErrorCode::Unsupported);
604        assert_eq!(typed.failure_reason.as_deref(), Some("not available here"));
605    }
606
607    #[test]
608    fn parses_invalid_request_errors_into_typed_details() {
609        let error = parse_framework_error(Some(
610            r#"{"kind":"invalidRequestError","code":47,"message":"bad request"}"#
611                .to_owned(),
612        ));
613        let typed = error
614            .invalid_request_error()
615            .expect("typed invalid request error");
616        assert_eq!(typed.code, 47);
617        assert_eq!(typed.message, "bad request");
618    }
619}