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