Skip to content
6 changes: 6 additions & 0 deletions android/jni/mob_erts.zig
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ pub inline fn enif_make_uint64(env: ?*ErlNifEnv, i: u64) ERL_NIF_TERM {
// enif_make_copy.
pub extern fn enif_alloc_env() ?*ErlNifEnv;
pub extern fn enif_free_env(env: ?*ErlNifEnv) void;
pub extern fn enif_alloc(size: usize) ?*anyopaque;
pub extern fn enif_free(ptr: ?*anyopaque) void;
pub extern fn enif_make_copy(dst: ?*ErlNifEnv, src_term: ERL_NIF_TERM) ERL_NIF_TERM;
pub extern fn enif_send(
caller_env: ?*ErlNifEnv,
Expand All @@ -209,6 +211,10 @@ pub extern fn enif_whereis_pid(env: ?*ErlNifEnv, name: ERL_NIF_TERM, pid: *ErlNi
// Tuple inspectors (iter 3c).
pub extern fn enif_get_tuple(env: ?*ErlNifEnv, tpl: ERL_NIF_TERM, arity: *c_int, array: *[*]const ERL_NIF_TERM) c_int;

// List inspectors.
pub extern fn enif_get_list_length(env: ?*ErlNifEnv, term: ERL_NIF_TERM, len: *c_uint) c_int;
pub extern fn enif_get_list_cell(env: ?*ErlNifEnv, term: ERL_NIF_TERM, head: *ERL_NIF_TERM, tail: *ERL_NIF_TERM) c_int;

// Mutex (iter 3c). enif_mutex_create allocates; destroy + try-lock omitted
// — Mob only uses simple lock/unlock pairs and the mutexes live for the
// lifetime of the BEAM process (no destroy needed).
Expand Down
136 changes: 136 additions & 0 deletions android/jni/mob_nif.zig
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ pub export var Bridge: BridgeMethods = .{};
extern var g_jvm: ?*jni.JavaVM;
extern var g_activity: jni.JObject;

// mob_iap plugin init — optional; iap.c short-circuits when plugin absent.
extern fn mob_iap_init(env: *jni.JNIEnv, activity: jni.JObject) callconv(.c) void;

// ── get_jenv: attach the current thread if needed ────────────────────────
// Returns the env pointer; *attached is set to 1 iff this call had to
// attach (caller must DetachCurrentThread when done). Match the C
Expand Down Expand Up @@ -4736,6 +4739,11 @@ fn nifLoad(env: ?*erts.ErlNifEnv, priv: *?*anyopaque, info: erts.ERL_NIF_TERM) c
return -1;
}

// mob_iap plugin — optional; iap.c short-circuits when class absent.
if (g_activity != null) {
mob_iap_init(jenv, g_activity);
}

g_launch_notif_mutex = erts.enif_mutex_create("mob_launch_notif_mutex");
if (g_launch_notif_mutex == null) {
loge_nif("nif_load: failed to create launch notif mutex", .{});
Expand All @@ -4760,6 +4768,128 @@ fn nifLoad(env: ?*erts.ErlNifEnv, priv: *?*anyopaque, info: erts.ERL_NIF_TERM) c
return 0;
}

// ── In-App Purchase NIF stubs (mob_iap plugin) ─────────────────────────
// Thin wrappers that extract the BEAM pid and product IDs, then delegate
// to the JNI bridge in iap.c. The actual StoreKit 2 / Play Billing work
// happens on the JVM/ObjC side.

// NOTE: iap.c JNI callbacks receive ErlNifPid* via jlong. They MUST
// call free() on that pointer after enif_send completes.
extern fn mob_iap_fetch_products(pid: *erts.ErlNifPid, ids: [*:null]const ?[*:0]const u8, count: c_int) void;
extern fn mob_iap_purchase(pid: *erts.ErlNifPid, product_id: [*:0]const u8) void;
extern fn mob_iap_restore(pid: *erts.ErlNifPid) void;
extern fn mob_iap_current_entitlements(pid: *erts.ErlNifPid) void;
extern fn mob_iap_manage_subscriptions() void;

fn nif_iap_fetch_products(
env: ?*erts.ErlNifEnv,
_: c_int,
argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
var pid: erts.ErlNifPid = undefined;
if (erts.enif_self(env, &pid) == null) {
return erts.badarg(env);
}

var list_len: c_uint = 0;
if (erts.enif_get_list_length(env, argv[0], &list_len) == 0) {
return erts.badarg(env);
}

const max = @min(list_len, 128);
var ids: [128]?[*:0]const u8 = @splat(null);
var head: erts.ERL_NIF_TERM = undefined;
var tail: erts.ERL_NIF_TERM = argv[0];
var buf: [4096]u8 = undefined;

for (0..max) |i| {
if (erts.enif_get_list_cell(env, tail, &head, &tail) == 0) {
for (0..i) |j| erts.enif_free(@ptrCast(@constCast(ids[j].?)));
return erts.badarg(env);
}
if (!fillBufferFromTerm(env, head, &buf)) {
for (0..i) |j| erts.enif_free(@ptrCast(@constCast(ids[j].?)));
return erts.badarg(env);
}
const cstr: [*:0]u8 = @ptrCast(&buf);
const len = std.mem.len(cstr) + 1;
const str = erts.enif_alloc(len);
if (str == null) {
for (0..i) |j| erts.enif_free(@ptrCast(@constCast(ids[j].?)));
return erts.badarg(env);
}
@memcpy(@as([*]u8, @ptrCast(str.?))[0..len], buf[0..len]);
ids[i] = @ptrCast(str);
}

const pid_ptr = erts.enif_alloc(@sizeOf(erts.ErlNifPid)) orelse return erts.badarg(env);
@as(*erts.ErlNifPid, @ptrCast(@alignCast(pid_ptr))).* = pid;
mob_iap_fetch_products(@ptrCast(@alignCast(pid_ptr)), @ptrCast(&ids[0]), @intCast(max));
return erts.atom(env, "ok");
}

fn nif_iap_purchase(
env: ?*erts.ErlNifEnv,
_: c_int,
argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
var pid: erts.ErlNifPid = undefined;
if (erts.enif_self(env, &pid) == null) {
return erts.badarg(env);
}

var buf: [4096]u8 = @splat(0);
if (!fillBufferFromTerm(env, argv[0], &buf)) {
return erts.badarg(env);
}

const pid_ptr = erts.enif_alloc(@sizeOf(erts.ErlNifPid)) orelse return erts.badarg(env);
@as(*erts.ErlNifPid, @ptrCast(@alignCast(pid_ptr))).* = pid;
mob_iap_purchase(@ptrCast(@alignCast(pid_ptr)), @ptrCast(&buf));
return erts.atom(env, "ok");
}

fn nif_iap_restore(
env: ?*erts.ErlNifEnv,
_: c_int,
_: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
var pid: erts.ErlNifPid = undefined;
if (erts.enif_self(env, &pid) == null) {
return erts.badarg(env);
}

const pid_ptr = erts.enif_alloc(@sizeOf(erts.ErlNifPid)) orelse return erts.badarg(env);
@as(*erts.ErlNifPid, @ptrCast(@alignCast(pid_ptr))).* = pid;
mob_iap_restore(@ptrCast(@alignCast(pid_ptr)));
return erts.atom(env, "ok");
}

fn nif_iap_current_entitlements(
env: ?*erts.ErlNifEnv,
_: c_int,
_: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
var pid: erts.ErlNifPid = undefined;
if (erts.enif_self(env, &pid) == null) {
return erts.badarg(env);
}

const pid_ptr = erts.enif_alloc(@sizeOf(erts.ErlNifPid)) orelse return erts.badarg(env);
@as(*erts.ErlNifPid, @ptrCast(@alignCast(pid_ptr))).* = pid;
mob_iap_current_entitlements(@ptrCast(@alignCast(pid_ptr)));
return erts.atom(env, "ok");
}

fn nif_iap_manage_subscriptions(
env: ?*erts.ErlNifEnv,
_: c_int,
_: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
mob_iap_manage_subscriptions();
return erts.atom(env, "ok");
}

// ── NIF table + ERL_NIF_INIT entry point ─────────────────────────────────
// Replaces the static `ErlNifFunc nif_funcs[]` + `ERL_NIF_INIT` macro
// that used to live at the bottom of mob_nif.c. The entry point is the
Expand Down Expand Up @@ -4874,6 +5004,12 @@ const nif_funcs = [_]erts.ErlNifFunc{
.{ .name = "bt_spp_write", .arity = 2, .fptr = nif_bt_spp_write, .flags = erts.ERL_NIF_DIRTY_JOB_IO_BOUND },
.{ .name = "bt_hid_connect", .arity = 1, .fptr = nif_bt_hid_connect, .flags = 0 },
.{ .name = "bt_hid_subscribe_raw", .arity = 1, .fptr = nif_bt_hid_subscribe_raw, .flags = 0 },
// ── In-App Purchase (mob_iap plugin) ──────────────────────────────────────
.{ .name = "iap_fetch_products", .arity = 1, .fptr = nif_iap_fetch_products, .flags = 0 },
.{ .name = "iap_purchase", .arity = 1, .fptr = nif_iap_purchase, .flags = 0 },
.{ .name = "iap_restore", .arity = 0, .fptr = nif_iap_restore, .flags = 0 },
.{ .name = "iap_current_entitlements", .arity = 0, .fptr = nif_iap_current_entitlements, .flags = 0 },
.{ .name = "iap_manage_subscriptions", .arity = 0, .fptr = nif_iap_manage_subscriptions, .flags = 0 },
};

var mob_nif_entry: erts.ErlNifEntry = .{
Expand Down
195 changes: 195 additions & 0 deletions ios/mob_nif.m
Original file line number Diff line number Diff line change
Expand Up @@ -2039,6 +2039,195 @@ static void mob_send3(const ErlNifPid *pid, const char *a1, const char *a2, cons
enif_free_env(e);
}

// ════════════════════════════════════════════════════════════════════════════
// IAP bridge helpers — called from MobIapBridge.swift via @_silgen_name
//
// pid_bytes lifetime contract: every mob_iap_send_* below frees pid_bytes
// before returning. The Swift bridge MUST allocate one ErlNifPid copy per
// pending operation and call exactly one mob_iap_send_* with it. Calling
// two send helpers with the same pid_bytes is a use-after-free; storing
// pid_bytes after a send is a use-after-free. If a single user action
// could produce multiple events (e.g. :purchase_pending then :purchased),
// the bridge must store the BEAM pid value itself (not the heap copy)
// and allocate a fresh ErlNifPid for each enif_send.
//
// Symbols are intentionally non-static: Swift's @_silgen_name needs to
// link them across the bridging-header-less Swift/ObjC NIF boundary.
// ════════════════════════════════════════════════════════════════════════════

// Send {:iap, atom} to the BEAM process identified by serialized ErlNifPid.
// pid_bytes points to a serialized ErlNifPid copied by the NIF caller.
void mob_iap_send2(const void *pid_bytes, const char *tag, const char *atom) {
ErlNifPid pid;
memcpy(&pid, pid_bytes, sizeof(ErlNifPid));
ErlNifEnv *e = enif_alloc_env();
ERL_NIF_TERM msg = enif_make_tuple2(e, enif_make_atom(e, tag), enif_make_atom(e, atom));
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
free((void *)pid_bytes);
}

// Send {:iap, tag, atom} to the BEAM.
void mob_iap_send3(const void *pid_bytes, const char *tag, const char *a1, const char *a2) {
ErlNifPid pid;
memcpy(&pid, pid_bytes, sizeof(ErlNifPid));
ErlNifEnv *e = enif_alloc_env();
ERL_NIF_TERM msg =
enif_make_tuple3(e, enif_make_atom(e, tag), enif_make_atom(e, a1), enif_make_atom(e, a2));
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
free((void *)pid_bytes);
}

// Send {:iap, :products, binary_json} — JSON list of product maps.
void mob_iap_send_products(const void *pid_bytes, const char *json) {
ErlNifPid pid;
memcpy(&pid, pid_bytes, sizeof(ErlNifPid));
ErlNifEnv *e = enif_alloc_env();
ERL_NIF_TERM json_bin;
size_t len = strlen(json);
unsigned char *buf = enif_make_new_binary(e, len, &json_bin);
memcpy(buf, json, len);
ERL_NIF_TERM msg =
enif_make_tuple3(e, enif_make_atom(e, "iap"), enif_make_atom(e, "products"), json_bin);
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
free((void *)pid_bytes);
}

// Send {:iap, tag, binary_json} — a single transaction as JSON map.
void mob_iap_send_transaction(const void *pid_bytes, const char *tag, const char *json) {
ErlNifPid pid;
memcpy(&pid, pid_bytes, sizeof(ErlNifPid));
ErlNifEnv *e = enif_alloc_env();
ERL_NIF_TERM json_bin;
size_t len = strlen(json);
unsigned char *buf = enif_make_new_binary(e, len, &json_bin);
memcpy(buf, json, len);
ERL_NIF_TERM msg =
enif_make_tuple3(e, enif_make_atom(e, "iap"), enif_make_atom(e, tag), json_bin);
enif_send(NULL, &pid, e, msg);
enif_free_env(e);
free((void *)pid_bytes);
}

// Send {:iap, tag, binary_json} — a JSON array of transactions.
void mob_iap_send_transactions(const void *pid_bytes, const char *tag, const char *json) {
mob_iap_send_transaction(pid_bytes, tag, json);
}

// ════════════════════════════════════════════════════════════════════════════
// IAP NIF function implementations
// ════════════════════════════════════════════════════════════════════════════

// Extract the product IDs list from a term — expects a list of binaries.
// On failure, frees any ids already allocated and returns 0.
static size_t iap_extract_product_ids(ErlNifEnv *env, ERL_NIF_TERM list, char **ids,
size_t max_ids) {
unsigned int list_len;
if (!enif_get_list_length(env, list, &list_len))
return 0;
if (list_len > max_ids)
return 0;

ERL_NIF_TERM head, tail = list;
for (unsigned int i = 0; i < list_len; i++) {
if (!enif_get_list_cell(env, tail, &head, &tail)) {
for (unsigned int j = 0; j < i; j++)
free(ids[j]);
return 0;
}
ErlNifBinary bin;
if (!enif_inspect_binary(env, head, &bin) &&
!enif_inspect_iolist_as_binary(env, head, &bin)) {
for (unsigned int j = 0; j < i; j++)
free(ids[j]);
return 0;
}
ids[i] = strndup((const char *)bin.data, bin.size);
}
return list_len;
}

static ERL_NIF_TERM nif_iap_fetch_products(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 1)
return enif_make_badarg(env);

char *ids[128];
memset(ids, 0, sizeof(ids));
size_t count = iap_extract_product_ids(env, argv[0], ids, 128);
// iap_extract_product_ids returns 0 on failure AND frees any partial
// allocations itself. An empty input list is also 0 — which is a
// valid edge case but useless work, so we reject both with badarg.
if (count == 0)
return enif_make_badarg(env);

ErlNifPid *pid_copy = malloc(sizeof(ErlNifPid));
enif_self(env, pid_copy);

// Build NSArray<NSString *> and pass to MobIapBridge
NSMutableArray *productIds = [NSMutableArray arrayWithCapacity:count];
for (size_t i = 0; i < count; i++) {
[productIds addObject:[NSString stringWithUTF8String:ids[i]]];
free(ids[i]);
}

[MobIapBridge fetchProducts:productIds pidBytes:pid_copy];
return enif_make_atom(env, "ok");
}

static ERL_NIF_TERM nif_iap_purchase(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 1)
return enif_make_badarg(env);

ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[0], &bin) &&
!enif_inspect_iolist_as_binary(env, argv[0], &bin))
return enif_make_badarg(env);

NSString *productId = [[NSString alloc] initWithBytes:bin.data
length:bin.size
encoding:NSUTF8StringEncoding];

ErlNifPid *pid_copy = malloc(sizeof(ErlNifPid));
enif_self(env, pid_copy);

[MobIapBridge purchase:productId pidBytes:pid_copy];
return enif_make_atom(env, "ok");
}

static ERL_NIF_TERM nif_iap_restore(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 0)
return enif_make_badarg(env);

ErlNifPid *pid_copy = malloc(sizeof(ErlNifPid));
enif_self(env, pid_copy);

[MobIapBridge restorePurchases:pid_copy];
return enif_make_atom(env, "ok");
}

static ERL_NIF_TERM nif_iap_current_entitlements(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
if (argc != 0)
return enif_make_badarg(env);

ErlNifPid *pid_copy = malloc(sizeof(ErlNifPid));
enif_self(env, pid_copy);

[MobIapBridge currentEntitlements:pid_copy];
return enif_make_atom(env, "ok");
}

static ERL_NIF_TERM nif_iap_manage_subscriptions(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
if (argc != 0)
return enif_make_badarg(env);

[MobIapBridge manageSubscriptions];
return enif_make_atom(env, "ok");
}

// Return the root view controller of the key window in the first active scene.
static UIViewController *mob_root_vc(void) {
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
Expand Down Expand Up @@ -6651,6 +6840,12 @@ static ERL_NIF_TERM nif_bt_hid_subscribe_raw(ErlNifEnv *env, int argc, const ERL
// doesn't head-of-line-block the regular schedulers. See the impl
// above for the iOS rationale.
{"resolve_ipv4", 1, nif_resolve_ipv4, ERL_NIF_DIRTY_JOB_IO_BOUND},
// ── In-App Purchase (mob_iap plugin) ──────────────────────────────────────
{"iap_fetch_products", 1, nif_iap_fetch_products, 0},
{"iap_purchase", 1, nif_iap_purchase, 0},
{"iap_restore", 0, nif_iap_restore, 0},
{"iap_current_entitlements", 0, nif_iap_current_entitlements, 0},
{"iap_manage_subscriptions", 0, nif_iap_manage_subscriptions, 0},
};

static int nif_load(ErlNifEnv *env, void **priv, ERL_NIF_TERM info) {
Expand Down
Loading