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}