1use core::ffi::c_void;
2use core::ptr;
3use std::ptr::NonNull;
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7
8use crate::advanced_commerce::{
9 TransactionAdvancedCommerceInfo, TransactionAdvancedCommerceInfoPayload,
10};
11use crate::app_store::AppStoreEnvironment;
12use crate::error::{StoreKitError, VerificationFailure};
13use crate::ffi;
14use crate::private::{
15 cstring_from_str, decode_base64, duration_to_timeout_ms, error_from_status, json_cstring,
16 parse_json_ptr, parse_optional_json_ptr,
17};
18use crate::product::ProductType;
19use crate::refund::{Refund, RefundRequestStatus};
20use crate::storefront::{Storefront, StorefrontPayload};
21use crate::subscription::{SubscriptionPeriod, SubscriptionPeriodPayload};
22pub use crate::verification_result::VerificationResult;
23
24use crate::verification_result::VerificationResultPayload;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum TransactionReason {
28 Purchase,
29 Renewal,
30 Unknown(String),
31}
32
33impl TransactionReason {
34 pub fn as_str(&self) -> &str {
35 match self {
36 Self::Purchase => "purchase",
37 Self::Renewal => "renewal",
38 Self::Unknown(value) => value.as_str(),
39 }
40 }
41
42 fn from_raw(raw: String) -> Self {
43 match raw.as_str() {
44 "purchase" => Self::Purchase,
45 "renewal" => Self::Renewal,
46 _ => Self::Unknown(raw),
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum RevocationReason {
53 DeveloperIssue,
54 Other,
55 Unknown(String),
56}
57
58impl RevocationReason {
59 pub fn as_str(&self) -> &str {
60 match self {
61 Self::DeveloperIssue => "developerIssue",
62 Self::Other => "other",
63 Self::Unknown(value) => value.as_str(),
64 }
65 }
66
67 fn from_raw(raw: String) -> Self {
68 match raw.as_str() {
69 "developerIssue" => Self::DeveloperIssue,
70 "other" => Self::Other,
71 _ => Self::Unknown(raw),
72 }
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum OfferType {
78 Introductory,
79 Promotional,
80 Code,
81 WinBack,
82 Unknown(String),
83}
84
85impl OfferType {
86 pub fn as_str(&self) -> &str {
87 match self {
88 Self::Introductory => "introductory",
89 Self::Promotional => "promotional",
90 Self::Code => "code",
91 Self::WinBack => "winBack",
92 Self::Unknown(value) => value.as_str(),
93 }
94 }
95
96 fn from_raw(raw: String) -> Self {
97 match raw.as_str() {
98 "introductory" => Self::Introductory,
99 "promotional" => Self::Promotional,
100 "code" => Self::Code,
101 "winBack" => Self::WinBack,
102 _ => Self::Unknown(raw),
103 }
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum OfferPaymentMode {
109 FreeTrial,
110 PayAsYouGo,
111 PayUpFront,
112 OneTime,
113 Unknown(String),
114}
115
116impl OfferPaymentMode {
117 pub fn as_str(&self) -> &str {
118 match self {
119 Self::FreeTrial => "freeTrial",
120 Self::PayAsYouGo => "payAsYouGo",
121 Self::PayUpFront => "payUpFront",
122 Self::OneTime => "oneTime",
123 Self::Unknown(value) => value.as_str(),
124 }
125 }
126
127 fn from_raw(raw: String) -> Self {
128 match raw.as_str() {
129 "freeTrial" => Self::FreeTrial,
130 "payAsYouGo" => Self::PayAsYouGo,
131 "payUpFront" => Self::PayUpFront,
132 "oneTime" => Self::OneTime,
133 _ => Self::Unknown(raw),
134 }
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct TransactionOffer {
140 pub id: Option<String>,
141 pub offer_type: OfferType,
142 pub payment_mode: Option<OfferPaymentMode>,
143 pub period: Option<SubscriptionPeriod>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub enum OwnershipType {
148 Purchased,
149 FamilyShared,
150 Unknown(String),
151}
152
153impl OwnershipType {
154 fn from_raw(raw: String) -> Self {
155 match raw.as_str() {
156 "purchased" => Self::Purchased,
157 "familyShared" => Self::FamilyShared,
158 _ => Self::Unknown(raw),
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct TransactionData {
165 pub id: u64,
166 pub original_id: u64,
167 pub web_order_line_item_id: Option<String>,
168 pub product_id: String,
169 pub subscription_group_id: Option<String>,
170 pub app_bundle_id: String,
171 pub purchase_date: String,
172 pub original_purchase_date: String,
173 pub expiration_date: Option<String>,
174 pub purchased_quantity: u64,
175 pub is_upgraded: bool,
176 pub ownership_type: OwnershipType,
177 pub signed_date: String,
178 pub jws_representation: String,
179 pub verification_failure: Option<VerificationFailure>,
180 pub revocation_date: Option<String>,
181 pub revocation_reason: Option<RevocationReason>,
182 pub product_type: Option<ProductType>,
183 pub app_account_token: Option<String>,
184 pub environment: Option<AppStoreEnvironment>,
185 pub reason: Option<TransactionReason>,
186 pub storefront: Option<Storefront>,
187 pub price: Option<String>,
188 pub currency_code: Option<String>,
189 pub app_transaction_id: Option<String>,
190 pub offer: Option<TransactionOffer>,
191 pub json_representation: Vec<u8>,
192}
193
194#[derive(Debug)]
195pub struct Transaction {
196 handle: Option<NonNull<c_void>>,
197 data: TransactionData,
198 advanced_commerce_info: Option<TransactionAdvancedCommerceInfo>,
199}
200
201impl Clone for Transaction {
202 fn clone(&self) -> Self {
203 let handle = self.handle.map(|handle| {
204 let retained = unsafe { ffi::sk_transaction_retain(handle.as_ptr()) };
205 NonNull::new(retained).expect("StoreKit transaction retain returned null")
206 });
207 Self {
208 handle,
209 data: self.data.clone(),
210 advanced_commerce_info: self.advanced_commerce_info.clone(),
211 }
212 }
213}
214
215impl Drop for Transaction {
216 fn drop(&mut self) {
217 if let Some(handle) = self.handle {
218 unsafe { ffi::sk_transaction_release(handle.as_ptr()) };
219 }
220 }
221}
222
223impl Transaction {
224 pub fn current_entitlements() -> Result<TransactionStream, StoreKitError> {
225 TransactionStream::new(&TransactionStreamConfig::current_entitlements())
226 }
227
228 pub fn all() -> Result<TransactionStream, StoreKitError> {
229 TransactionStream::new(&TransactionStreamConfig::all())
230 }
231
232 pub fn updates() -> Result<TransactionStream, StoreKitError> {
233 TransactionStream::new(&TransactionStreamConfig::updates())
234 }
235
236 pub fn unfinished() -> Result<TransactionStream, StoreKitError> {
237 TransactionStream::new(&TransactionStreamConfig::unfinished())
238 }
239
240 pub fn all_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
241 TransactionStream::new(&TransactionStreamConfig::all_for(product_id))
242 }
243
244 pub fn current_entitlements_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
245 TransactionStream::new(&TransactionStreamConfig::current_entitlements_for(
246 product_id,
247 ))
248 }
249
250 pub fn latest_for(product_id: &str) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
251 let product_id = cstring_from_str(product_id, "product id")?;
252 let mut transaction_handle = ptr::null_mut();
253 let mut result_json = ptr::null_mut();
254 let mut error_message = ptr::null_mut();
255 let status = unsafe {
256 ffi::sk_transaction_latest_for(
257 product_id.as_ptr(),
258 &mut transaction_handle,
259 &mut result_json,
260 &mut error_message,
261 )
262 };
263 if status != ffi::status::OK {
264 return Err(unsafe { error_from_status(status, error_message) });
265 }
266
267 let payload = unsafe {
268 parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
269 result_json,
270 "latest transaction",
271 )
272 }?;
273 payload
274 .map(|payload| {
275 payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
276 })
277 .transpose()
278 }
279
280 pub fn current_entitlement_for(
281 product_id: &str,
282 ) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
283 let product_id = cstring_from_str(product_id, "product id")?;
284 let mut transaction_handle = ptr::null_mut();
285 let mut result_json = ptr::null_mut();
286 let mut error_message = ptr::null_mut();
287 let status = unsafe {
288 ffi::sk_transaction_current_entitlement_for(
289 product_id.as_ptr(),
290 &mut transaction_handle,
291 &mut result_json,
292 &mut error_message,
293 )
294 };
295 if status != ffi::status::OK {
296 return Err(unsafe { error_from_status(status, error_message) });
297 }
298
299 let payload = unsafe {
300 parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
301 result_json,
302 "current entitlement transaction",
303 )
304 }?;
305 payload
306 .map(|payload| {
307 payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
308 })
309 .transpose()
310 }
311
312 pub const fn data(&self) -> &TransactionData {
313 &self.data
314 }
315
316 pub const fn advanced_commerce_info(&self) -> Option<&TransactionAdvancedCommerceInfo> {
317 self.advanced_commerce_info.as_ref()
318 }
319
320 pub const fn has_live_handle(&self) -> bool {
321 self.handle.is_some()
322 }
323
324 pub fn verify(&self) -> Result<(), StoreKitError> {
325 self.handle.map_or_else(
326 || {
327 self.data
328 .verification_failure
329 .clone()
330 .map_or(Ok(()), |failure| Err(StoreKitError::Verification(failure)))
331 },
332 |handle| {
333 let mut error_message = ptr::null_mut();
334 let status =
335 unsafe { ffi::sk_transaction_verify(handle.as_ptr(), &mut error_message) };
336 if status == ffi::status::OK {
337 Ok(())
338 } else {
339 Err(unsafe { error_from_status(status, error_message) })
340 }
341 },
342 )
343 }
344
345 pub fn finish(&self) -> Result<(), StoreKitError> {
346 self.handle.map_or_else(
347 || {
348 Err(StoreKitError::NotSupported(
349 "transaction snapshots cannot be finished because they do not carry a live StoreKit handle"
350 .to_owned(),
351 ))
352 },
353 |handle| {
354 let mut error_message = ptr::null_mut();
355 let status = unsafe { ffi::sk_transaction_finish(handle.as_ptr(), &mut error_message) };
356 if status == ffi::status::OK {
357 Ok(())
358 } else {
359 Err(unsafe { error_from_status(status, error_message) })
360 }
361 },
362 )
363 }
364
365 pub fn begin_refund_request(&self) -> Result<RefundRequestStatus, StoreKitError> {
366 Refund::begin_for_transaction_id(self.data.id)
367 }
368
369 pub(crate) fn from_raw_parts(
370 handle: *mut c_void,
371 payload: TransactionPayload,
372 ) -> Result<Self, StoreKitError> {
373 let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
374 Ok(Self {
375 handle: NonNull::new(handle),
376 data,
377 advanced_commerce_info,
378 })
379 }
380
381 pub(crate) fn from_snapshot_payload(
382 payload: TransactionPayload,
383 ) -> Result<Self, StoreKitError> {
384 let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
385 Ok(Self {
386 handle: None,
387 data,
388 advanced_commerce_info,
389 })
390 }
391}
392
393#[derive(Debug)]
394pub struct TransactionStream {
395 handle: NonNull<c_void>,
396 finished: bool,
397}
398
399impl Drop for TransactionStream {
400 fn drop(&mut self) {
401 unsafe { ffi::sk_transaction_stream_release(self.handle.as_ptr()) };
402 }
403}
404
405impl TransactionStream {
406 fn new(config: &TransactionStreamConfig) -> Result<Self, StoreKitError> {
407 let config_json = json_cstring(config, "transaction stream config")?;
408 let mut error_message = ptr::null_mut();
409 let handle =
410 unsafe { ffi::sk_transaction_stream_create(config_json.as_ptr(), &mut error_message) };
411 let handle = NonNull::new(handle)
412 .ok_or_else(|| unsafe { error_from_status(ffi::status::UNKNOWN, error_message) })?;
413 Ok(Self {
414 handle,
415 finished: false,
416 })
417 }
418
419 pub const fn is_finished(&self) -> bool {
420 self.finished
421 }
422
423 #[allow(clippy::should_implement_trait)]
424 pub fn next(&mut self) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
425 self.next_timeout(Duration::from_secs(30))
426 }
427
428 pub fn next_timeout(
429 &mut self,
430 timeout: Duration,
431 ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
432 let mut transaction_handle = ptr::null_mut();
433 let mut verification_json = ptr::null_mut();
434 let mut error_message = ptr::null_mut();
435 let status = unsafe {
436 ffi::sk_transaction_stream_next(
437 self.handle.as_ptr(),
438 duration_to_timeout_ms(timeout),
439 &mut transaction_handle,
440 &mut verification_json,
441 &mut error_message,
442 )
443 };
444
445 match status {
446 ffi::status::OK => {
447 let payload = unsafe {
448 parse_json_ptr::<VerificationResultPayload<TransactionPayload>>(
449 verification_json,
450 "transaction verification result",
451 )
452 };
453 match payload {
454 Ok(payload) => payload
455 .into_result(|payload| {
456 Transaction::from_raw_parts(transaction_handle, payload)
457 })
458 .map(Some),
459 Err(error) => {
460 if !transaction_handle.is_null() {
461 unsafe { ffi::sk_transaction_release(transaction_handle) };
462 }
463 Err(error)
464 }
465 }
466 }
467 ffi::status::END_OF_STREAM => {
468 self.finished = true;
469 Ok(None)
470 }
471 ffi::status::TIMED_OUT => Ok(None),
472 _ => Err(unsafe { error_from_status(status, error_message) }),
473 }
474 }
475}
476
477#[derive(Debug, Serialize)]
478struct TransactionStreamConfig {
479 kind: &'static str,
480 #[serde(rename = "productID", skip_serializing_if = "Option::is_none")]
481 product_id: Option<String>,
482}
483
484impl TransactionStreamConfig {
485 const fn all() -> Self {
486 Self {
487 kind: "all",
488 product_id: None,
489 }
490 }
491
492 const fn current_entitlements() -> Self {
493 Self {
494 kind: "currentEntitlements",
495 product_id: None,
496 }
497 }
498
499 const fn updates() -> Self {
500 Self {
501 kind: "updates",
502 product_id: None,
503 }
504 }
505
506 const fn unfinished() -> Self {
507 Self {
508 kind: "unfinished",
509 product_id: None,
510 }
511 }
512
513 fn all_for(product_id: &str) -> Self {
514 Self {
515 kind: "allFor",
516 product_id: Some(product_id.to_owned()),
517 }
518 }
519
520 fn current_entitlements_for(product_id: &str) -> Self {
521 Self {
522 kind: "currentEntitlementsFor",
523 product_id: Some(product_id.to_owned()),
524 }
525 }
526}
527
528#[derive(Debug, Deserialize)]
529pub(crate) struct TransactionOfferPayload {
530 id: Option<String>,
531 #[serde(rename = "type")]
532 offer_type: String,
533 #[serde(rename = "paymentMode")]
534 payment_mode: Option<String>,
535 period: Option<SubscriptionPeriodPayload>,
536}
537
538impl TransactionOfferPayload {
539 pub(crate) fn into_transaction_offer(self) -> TransactionOffer {
540 TransactionOffer {
541 id: self.id,
542 offer_type: OfferType::from_raw(self.offer_type),
543 payment_mode: self.payment_mode.map(OfferPaymentMode::from_raw),
544 period: self
545 .period
546 .map(SubscriptionPeriodPayload::into_subscription_period),
547 }
548 }
549}
550
551#[derive(Debug, Deserialize)]
552pub(crate) struct TransactionPayload {
553 id: u64,
554 #[serde(rename = "originalID")]
555 original_id: u64,
556 #[serde(rename = "webOrderLineItemID")]
557 web_order_line_item_id: Option<String>,
558 #[serde(rename = "productID")]
559 product_id: String,
560 #[serde(rename = "subscriptionGroupID")]
561 subscription_group_id: Option<String>,
562 #[serde(rename = "appBundleID")]
563 app_bundle_id: String,
564 #[serde(rename = "purchaseDate")]
565 purchase_date: String,
566 #[serde(rename = "originalPurchaseDate")]
567 original_purchase_date: String,
568 #[serde(rename = "expirationDate")]
569 expiration_date: Option<String>,
570 #[serde(rename = "purchasedQuantity")]
571 purchased_quantity: u64,
572 #[serde(rename = "isUpgraded")]
573 is_upgraded: bool,
574 #[serde(rename = "ownershipType")]
575 ownership_type: String,
576 #[serde(rename = "signedDate")]
577 signed_date: String,
578 #[serde(rename = "jwsRepresentation")]
579 jws_representation: String,
580 #[serde(rename = "verificationError")]
581 verification_error: Option<crate::error::VerificationErrorPayload>,
582 #[serde(rename = "revocationDate")]
583 revocation_date: Option<String>,
584 #[serde(rename = "revocationReason")]
585 revocation_reason: Option<String>,
586 #[serde(rename = "productType")]
587 product_type: Option<String>,
588 #[serde(rename = "appAccountToken")]
589 app_account_token: Option<String>,
590 environment: Option<String>,
591 reason: Option<String>,
592 storefront: Option<StorefrontPayload>,
593 price: Option<String>,
594 #[serde(rename = "currencyCode")]
595 currency_code: Option<String>,
596 #[serde(rename = "appTransactionID")]
597 app_transaction_id: Option<String>,
598 offer: Option<TransactionOfferPayload>,
599 #[serde(rename = "advancedCommerceInfo")]
600 advanced_commerce_info: Option<TransactionAdvancedCommerceInfoPayload>,
601 #[serde(rename = "jsonRepresentationBase64")]
602 json_representation_base64: String,
603}
604
605impl TransactionPayload {
606 fn into_transaction_parts(
607 self,
608 ) -> Result<(TransactionData, Option<TransactionAdvancedCommerceInfo>), StoreKitError> {
609 let advanced_commerce_info = self
610 .advanced_commerce_info
611 .map(TransactionAdvancedCommerceInfoPayload::into_transaction_advanced_commerce_info);
612 Ok((TransactionData {
613 id: self.id,
614 original_id: self.original_id,
615 web_order_line_item_id: self.web_order_line_item_id,
616 product_id: self.product_id,
617 subscription_group_id: self.subscription_group_id,
618 app_bundle_id: self.app_bundle_id,
619 purchase_date: self.purchase_date,
620 original_purchase_date: self.original_purchase_date,
621 expiration_date: self.expiration_date,
622 purchased_quantity: self.purchased_quantity,
623 is_upgraded: self.is_upgraded,
624 ownership_type: OwnershipType::from_raw(self.ownership_type),
625 signed_date: self.signed_date,
626 jws_representation: self.jws_representation,
627 verification_failure: self
628 .verification_error
629 .map(crate::error::VerificationFailure::from_payload),
630 revocation_date: self.revocation_date,
631 revocation_reason: self.revocation_reason.map(RevocationReason::from_raw),
632 product_type: self.product_type.map(ProductType::from_raw),
633 app_account_token: self.app_account_token,
634 environment: self.environment.map(AppStoreEnvironment::from_raw),
635 reason: self.reason.map(TransactionReason::from_raw),
636 storefront: self.storefront.map(StorefrontPayload::into_storefront),
637 price: self.price,
638 currency_code: self.currency_code,
639 app_transaction_id: self.app_transaction_id,
640 offer: self
641 .offer
642 .map(TransactionOfferPayload::into_transaction_offer),
643 json_representation: decode_base64(
644 &self.json_representation_base64,
645 "transaction JSON representation",
646 )?,
647 }, advanced_commerce_info))
648 }
649}