storekit-rs 0.2.1

Safe Rust bindings for Apple's StoreKit framework — in-app purchases and transaction streams on macOS
Documentation
use core::ffi::c_void;
use core::ptr;
use std::ptr::NonNull;
use std::time::Duration;

use serde::Deserialize;

use crate::error::StoreKitError;
use crate::ffi;
use crate::private::{duration_to_timeout_ms, error_from_status, parse_json_ptr};
use crate::subscription_info::{SubscriptionStatus, SubscriptionStatusPayload};

#[derive(Debug, Clone)]
pub struct SubscriptionGroupStatuses {
    pub group_id: String,
    pub statuses: Vec<SubscriptionStatus>,
}

impl SubscriptionStatus {
    pub fn updates() -> Result<SubscriptionStatusStream, StoreKitError> {
        SubscriptionStatusStream::new()
    }

    pub fn all() -> Result<SubscriptionGroupStatusStream, StoreKitError> {
        SubscriptionGroupStatusStream::new()
    }
}

#[derive(Debug)]
pub struct SubscriptionStatusStream {
    handle: NonNull<c_void>,
    finished: bool,
}

impl Drop for SubscriptionStatusStream {
    fn drop(&mut self) {
        unsafe { ffi::sk_subscription_status_stream_release(self.handle.as_ptr()) };
    }
}

impl SubscriptionStatusStream {
    fn new() -> Result<Self, StoreKitError> {
        let mut error_message = ptr::null_mut();
        let handle = unsafe { ffi::sk_subscription_status_stream_create(&mut error_message) };
        let handle = NonNull::new(handle)
            .ok_or_else(|| unsafe { error_from_status(ffi::status::UNKNOWN, error_message) })?;
        Ok(Self {
            handle,
            finished: false,
        })
    }

    pub const fn is_finished(&self) -> bool {
        self.finished
    }

    #[allow(clippy::should_implement_trait)]
    pub fn next(&mut self) -> Result<Option<SubscriptionStatus>, StoreKitError> {
        self.next_timeout(Duration::from_secs(30))
    }

    pub fn next_timeout(&mut self, timeout: Duration) -> Result<Option<SubscriptionStatus>, StoreKitError> {
        let mut status_json = ptr::null_mut();
        let mut error_message = ptr::null_mut();
        let status = unsafe {
            ffi::sk_subscription_status_stream_next(
                self.handle.as_ptr(),
                duration_to_timeout_ms(timeout),
                &mut status_json,
                &mut error_message,
            )
        };

        match status {
            ffi::status::OK => {
                let payload = unsafe {
                    parse_json_ptr::<SubscriptionStatusPayload>(status_json, "subscription status update")
                }?;
                payload.into_subscription_status().map(Some)
            }
            ffi::status::END_OF_STREAM => {
                self.finished = true;
                Ok(None)
            }
            ffi::status::TIMED_OUT => Ok(None),
            _ => Err(unsafe { error_from_status(status, error_message) }),
        }
    }
}

#[derive(Debug)]
pub struct SubscriptionGroupStatusStream {
    handle: NonNull<c_void>,
    finished: bool,
}

impl Drop for SubscriptionGroupStatusStream {
    fn drop(&mut self) {
        unsafe { ffi::sk_subscription_group_status_stream_release(self.handle.as_ptr()) };
    }
}

impl SubscriptionGroupStatusStream {
    fn new() -> Result<Self, StoreKitError> {
        let mut error_message = ptr::null_mut();
        let handle = unsafe { ffi::sk_subscription_group_status_stream_create(&mut error_message) };
        let handle = NonNull::new(handle)
            .ok_or_else(|| unsafe { error_from_status(ffi::status::UNKNOWN, error_message) })?;
        Ok(Self {
            handle,
            finished: false,
        })
    }

    pub const fn is_finished(&self) -> bool {
        self.finished
    }

    #[allow(clippy::should_implement_trait)]
    pub fn next(&mut self) -> Result<Option<SubscriptionGroupStatuses>, StoreKitError> {
        self.next_timeout(Duration::from_secs(30))
    }

    pub fn next_timeout(
        &mut self,
        timeout: Duration,
    ) -> Result<Option<SubscriptionGroupStatuses>, StoreKitError> {
        let mut payload_json = ptr::null_mut();
        let mut error_message = ptr::null_mut();
        let status = unsafe {
            ffi::sk_subscription_group_status_stream_next(
                self.handle.as_ptr(),
                duration_to_timeout_ms(timeout),
                &mut payload_json,
                &mut error_message,
            )
        };

        match status {
            ffi::status::OK => {
                let payload = unsafe {
                    parse_json_ptr::<SubscriptionGroupStatusesPayload>(
                        payload_json,
                        "subscription group statuses",
                    )
                }?;
                payload.into_group_statuses().map(Some)
            }
            ffi::status::END_OF_STREAM => {
                self.finished = true;
                Ok(None)
            }
            ffi::status::TIMED_OUT => Ok(None),
            _ => Err(unsafe { error_from_status(status, error_message) }),
        }
    }
}

#[derive(Debug, Deserialize)]
struct SubscriptionGroupStatusesPayload {
    #[serde(rename = "groupID")]
    group_id: String,
    statuses: Vec<SubscriptionStatusPayload>,
}

impl SubscriptionGroupStatusesPayload {
    fn into_group_statuses(self) -> Result<SubscriptionGroupStatuses, StoreKitError> {
        Ok(SubscriptionGroupStatuses {
            group_id: self.group_id,
            statuses: self
                .statuses
                .into_iter()
                .map(SubscriptionStatusPayload::into_subscription_status)
                .collect::<Result<Vec<_>, _>>()?,
        })
    }
}