Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:

- name: Verify prebuilt bridges are staged
run: |
for triple in aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios aarch64-apple-darwin x86_64-apple-darwin; do
for triple in aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios aarch64-apple-tvos aarch64-apple-tvos-sim aarch64-apple-darwin x86_64-apple-darwin; do
test -s "crate-ios/prebuilt/$triple/libstorekit_bridge.a" || { echo "::error::missing bridge for $triple"; exit 1; }
done

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# @perryts/storekit

StoreKit 2 in-app purchase bindings (iOS / macOS) for [Perry](https://github.com/PerryTS/perry). For Android, use [`@perryts/play-billing`](https://github.com/PerryTS/play-billing).
StoreKit 2 in-app purchase bindings (iOS / tvOS / macOS) for [Perry](https://github.com/PerryTS/perry). For Android, use [`@perryts/play-billing`](https://github.com/PerryTS/play-billing).

## Platforms

| Target | Implementation |
| --------------- | ----------------------------------------------------------------------- |
| iOS 16+ | Native — Swift bridge over StoreKit 2 (`Product.products(for:)`, etc.). |
| tvOS 16+ | Native — same Swift bridge. |
| macOS 13+ | Native — same Swift bridge. |
| Android | Stub. Use [`@perryts/play-billing`](https://github.com/PerryTS/play-billing) for Google Play Billing. |
| Linux / Windows | Stub — every call resolves with a `"not available"` JSON payload. |
Expand Down
8 changes: 4 additions & 4 deletions crate-ios/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ repository = "https://github.com/PerryTS/storekit"
crate-type = ["staticlib", "rlib"]

[dependencies]
# Tracks the perry-ffi v0.5 ABI surface published from the
# `PerryTS/perry` workspace. Pinned to a git rev rather than a
# crates.io release until perry-ffi is published on crates.io.
perry-ffi = { git = "https://github.com/PerryTS/perry", branch = "main" }
# perry-ffi is pinned to the Perry release the package is tested with:
# `main` moves ahead of released compilers (and needs nightly since
# 0.6.0), and its ABI has to match the Perry that links the app.
perry-ffi = { git = "https://github.com/PerryTS/perry", tag = "v0.5.1664" }
22 changes: 19 additions & 3 deletions crate-ios/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ fn main() {
// Non-Apple target: lib.rs compiles the stub fallback instead of the
// Swift bridge, so there is nothing to build or link here. This keeps
// the crate buildable on hosts without Xcode/swiftc (e.g. Linux CI).
if target_os != "ios" && target_os != "macos" {
if target_os != "ios" && target_os != "macos" && target_os != "tvos" {
return;
}

Expand Down Expand Up @@ -48,6 +48,22 @@ fn main() {
"iphonesimulator".to_string(),
)
}
// Apple TV device (arm64)
t if t.contains("aarch64-apple-tvos") && !t.contains("sim") => {
let tvos_version = "16.0";
(
format!("arm64-apple-tvos{}", tvos_version),
"appletvos".to_string(),
)
}
// Apple TV simulator (arm64)
t if t.contains("aarch64-apple-tvos-sim") => {
let tvos_version = "16.0";
(
format!("arm64-apple-tvos{}-simulator", tvos_version),
"appletvsimulator".to_string(),
)
}
// macOS (arm64)
t if t.contains("aarch64-apple-darwin") => {
let macos_version = "13.0";
Expand Down Expand Up @@ -187,8 +203,8 @@ fn compile_bridge_from_source(

// Link Swift standard libraries
// Find the Swift library path for the target platform
let swift_lib_dir = if target_os == "ios" {
// For iOS, Swift libraries are in the SDK
let swift_lib_dir = if target_os == "ios" || target_os == "tvos" {
// For iOS and tvOS, Swift libraries are in the SDK
format!("{}/usr/lib/swift", sdk_path)
} else {
// For macOS, Swift libraries are in the toolchain
Expand Down
18 changes: 11 additions & 7 deletions crate-ios/src/apple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
//! as the callback "context", and returns the same pointer to the perry
//! runtime. When the Swift side finishes its
//! `Task { … }` it invokes [`storekit_callback`] from whatever thread the
//! Swift concurrency runtime parked the continuation on. The perry-ffi
//! resolution machinery is `Send`-safe and queues the actual fulfillment
//! back onto perry's main thread.
//! Swift concurrency runtime parked the continuation on. The callback copies
//! the result and settles the promise with `resolve_with`, so the JS string
//! is allocated on perry's main thread during the resolution pump.
//!
//! Swift returns a JSON string for every call; the TypeScript wrapper
//! parses it. We do not attempt to construct typed JsValues on the Rust
//! side — keeping the cross-language contract a single string makes the
//! Swift bridge trivial and avoids leaking perry-runtime layout into
//! Swift code.

use perry_ffi::{read_string, JsPromise, JsString, Promise, StringHeader};
use perry_ffi::{alloc_string, read_string, JsPromise, JsString, JsValue, Promise, StringHeader};
use std::ffi::CString;
use std::os::raw::{c_char, c_void};

Expand Down Expand Up @@ -56,14 +56,18 @@ extern "C" {
/// runtime carry the result back to the awaiter.
extern "C" fn storekit_callback(context: *mut c_void, result: *const c_char) {
let promise = unsafe { JsPromise::from_raw(context as *mut Promise) };
let result_str = if result.is_null() {
"{\"error\":\"null result from StoreKit bridge\"}"
let result_str: String = if result.is_null() {
"{\"error\":\"null result from StoreKit bridge\"}".to_owned()
} else {
unsafe { std::ffi::CStr::from_ptr(result) }
.to_str()
.unwrap_or("{\"error\":\"non-UTF8 result from StoreKit bridge\"}")
.to_owned()
};
promise.resolve_string(result_str);
// Swift calls back on a concurrency-runtime thread. Building the JS string
// there would allocate it in that thread's arena (undefined behaviour,
// perry#1824): copy the text here and allocate it on the main thread.
promise.resolve_with(move || JsValue::from_string_ptr(alloc_string(&result_str).as_raw()));
}

/// Read a perry JS string into an owned `CString` we can hand to Swift.
Expand Down
6 changes: 3 additions & 3 deletions crate-ios/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//! StoreKit 2 bindings for Perry — closes PerryTS/perry#537.
//!
//! On iOS and macOS targets this crate links the Swift StoreKit 2 bridge
//! On iOS, tvOS and macOS targets this crate links the Swift StoreKit 2 bridge
//! (see [`apple`]). On every other target it compiles to the same stub
//! behavior as `crate-stub` — no Swift toolchain, no Apple SDK — so the
//! crate builds on any machine with a Rust toolchain (`build.rs` skips
//! the `swiftc` step for non-Apple targets).

#[cfg(any(target_os = "ios", target_os = "macos"))]
#[cfg(any(target_os = "ios", target_os = "macos", target_os = "tvos"))]
mod apple;

#[cfg(not(any(target_os = "ios", target_os = "macos")))]
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "tvos")))]
mod stub;
2 changes: 1 addition & 1 deletion crate-stub/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ repository = "https://github.com/PerryTS/storekit"
crate-type = ["staticlib", "rlib"]

[dependencies]
perry-ffi = { git = "https://github.com/PerryTS/perry", branch = "main" }
perry-ffi = { git = "https://github.com/PerryTS/perry", tag = "v0.5.1664" }
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@perryts/storekit",
"version": "0.1.3",
"description": "StoreKit 2 in-app purchase bindings (iOS / macOS) for the Perry TypeScript-to-native compiler. For Android, see @perryts/play-billing.",
"version": "0.1.4",
"description": "StoreKit 2 in-app purchase bindings (iOS / tvOS / macOS) for the Perry TypeScript-to-native compiler. For Android, see @perryts/play-billing.",
"license": "MIT",
"repository": {
"type": "git",
Expand All @@ -18,6 +18,7 @@
"in-app-purchase",
"iap",
"ios",
"tvos",
"macos",
"subscriptions"
],
Expand Down Expand Up @@ -55,6 +56,11 @@
"lib": "libperry_storekit.a",
"frameworks": ["StoreKit", "Foundation"]
},
"tvos": {
"crate": "crate-ios",
"lib": "libperry_storekit.a",
"frameworks": ["StoreKit", "Foundation"]
},
"macos": {
"crate": "crate-ios",
"lib": "libperry_storekit.a",
Expand Down
4 changes: 3 additions & 1 deletion scripts/build-bridges.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Build the Swift StoreKit bridge for every Apple Rust target and stage the
# archives under crate-ios/prebuilt/<rust-triple>/libstorekit_bridge.a.
#
# Must run on macOS with Xcode (swiftc + the iOS/macOS SDKs). The archives
# Must run on macOS with Xcode (swiftc + the iOS/tvOS/macOS SDKs). The archives
# are shipped in the npm package so non-Mac hosts (Linux CI, perry's Linux
# builders) can produce iOS/macOS libraries without a Swift toolchain —
# crate-ios/build.rs links these when swiftc is unavailable.
Expand All @@ -21,6 +21,8 @@ TARGETS=(
"aarch64-apple-ios:arm64-apple-ios16.0:iphoneos"
"aarch64-apple-ios-sim:arm64-apple-ios16.0-simulator:iphonesimulator"
"x86_64-apple-ios:x86_64-apple-ios16.0-simulator:iphonesimulator"
"aarch64-apple-tvos:arm64-apple-tvos16.0:appletvos"
"aarch64-apple-tvos-sim:arm64-apple-tvos16.0-simulator:appletvsimulator"
"aarch64-apple-darwin:arm64-apple-macosx13.0:macosx"
"x86_64-apple-darwin:x86_64-apple-macosx13.0:macosx"
)
Expand Down
Loading