@crossbind/extension-activities 0.0.0-bootstrap.0 → 2.0.0-beta.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Buğra Sarı and crossbind contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,29 @@
1
1
  # @crossbind/extension-activities
2
2
 
3
- This version, 0.0.0-bootstrap.0, holds no code. npm lets a trusted publisher be configured only for a
4
- package that exists, so it reserves the name for the crossbind release workflow.
3
+ Handoff on iOS and macOS, for the user activities the page sets and continues.
5
4
 
6
- Install the real package: `npm install @crossbind/extension-activities@beta`. Source: https://github.com/crossbind/crossbind
5
+ An app has it by having `@crossbind/core-app-host`, which lists it as a dependency: there is nothing to install or list.
6
+
7
+ ## Use
8
+
9
+ ```js
10
+ import { app, events } from 'crossbind/app';
11
+
12
+ await app.setUserActivity({ type: 'com.example.notes.edit', userInfo: { id: 7 } });
13
+ events.on('user-activity', ({ type, userInfo }) => open(type, userInfo));
14
+ ```
15
+
16
+ ## Options
17
+
18
+ `app.activityTypes` lists the types of user activities the app sets and continues, as `['com.example.notes.edit']`. The
19
+ app has this extension only where a type is listed.
20
+
21
+ ## Platforms
22
+
23
+ iOS and macOS. Elsewhere `app.setUserActivity` rejects.
24
+
25
+ ## Replace or leave out
26
+
27
+ It holds the role `activities`. An extension that gives `replaces: 'activities'` takes it; `app: { without: ['activities'] }` in `crossbind.config.js` leaves it out. Leaving it out while `app.activityTypes` is set stops the build. [The guide](https://crossbind.dev/guide/extensions/#replace) shows how, and [extensions.md](https://github.com/crossbind/crossbind/blob/main/docs/api/extensions.md#the-hosts-own-extensions) what a replacement answers.
28
+
29
+ [app.md](https://github.com/crossbind/crossbind/blob/main/docs/api/app.md#user-activities) has the rules of a type.
@@ -0,0 +1,17 @@
1
+ export default function activities() {
2
+ return {
3
+ name: 'activities',
4
+ contract: 1,
5
+ root: import.meta.dirname,
6
+ reads: ['activityTypes'],
7
+ // The app's own types, whose activities it hands the page, on the systems that continue them.
8
+ fromApp(api) {
9
+ const types = api.config.app.activityTypes;
10
+ return ['macos', 'ios'].includes(api.platform) && types?.length > 0 ? { types: [...types] } : null;
11
+ },
12
+ app: {
13
+ macos: { sources: ['native/apple.mm'] },
14
+ ios: { sources: ['native/apple.mm'] },
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,98 @@
1
+ // The user activities of app.activityTypes, the app's own, whose types the build gives this extension: the page hears
2
+ // of each the system continues the app with as the held event user-activity, and makes one current, for its user's other
3
+ // devices to continue, with crossbind/app's app.setUserActivity(), which takes those types alone.
4
+ #import <Foundation/Foundation.h>
5
+
6
+ #include "crossbind/app.h"
7
+ #include "crossbind/extension.h"
8
+
9
+ #include <algorithm>
10
+ #include <stdexcept>
11
+ #include <string>
12
+ #include <vector>
13
+
14
+ namespace {
15
+
16
+ using crossbind::Value;
17
+
18
+ // The activity the page made current, which stays current as long as it lives.
19
+ NSUserActivity *current;
20
+
21
+ NSString *stringOf(const std::string &text) {
22
+ NSString *string = [[NSString alloc] initWithBytes:text.data() length:text.size() encoding:NSUTF8StringEncoding];
23
+ if (!string) throw std::invalid_argument("crossbind: app.setUserActivity takes text of UTF-8 alone");
24
+ return string;
25
+ }
26
+
27
+ // A JSON value or bytes as a property list's, which a user activity's userInfo holds.
28
+ id plistOf(const Value &value) {
29
+ if (value.isBool()) return @(value.asBool());
30
+ if (value.isNumber()) return @(value.asNumber());
31
+ if (value.isString()) return stringOf(value.asString());
32
+ if (value.isBytes()) return [NSData dataWithBytes:value.asBytes().data() length:value.asBytes().size()];
33
+ if (value.isArray()) {
34
+ NSMutableArray *items = [NSMutableArray array];
35
+ for (const Value &item : value.asArray()) [items addObject:plistOf(item)];
36
+ return items;
37
+ }
38
+ if (value.isObject()) {
39
+ NSMutableDictionary *members = [NSMutableDictionary dictionary];
40
+ for (const auto &[key, member] : value.asObject()) members[stringOf(key)] = plistOf(member);
41
+ return members;
42
+ }
43
+ return NSNull.null;
44
+ }
45
+
46
+ } // namespace
47
+
48
+ void crossbind_extension_activities(crossbind::Registry &registry) {
49
+ using crossbind::Call;
50
+ // JSON has no bytes, which another app's activity may hold: the host tells why the page hears nothing of one.
51
+ registry.onUserActivity([](const crossbind::UserActivity &activity) {
52
+ Value::Object event{{"type", activity.type}, {"userInfo", activity.userInfo}};
53
+ if (!activity.webpageUrl.empty()) event.emplace_back("webpageUrl", activity.webpageUrl);
54
+ crossbind::app::holdEvent("user-activity", Value(std::move(event)).json());
55
+ return true;
56
+ });
57
+ std::vector<std::string> types;
58
+ for (const Value &type : registry.options()["types"].asArray()) types.push_back(type.asString());
59
+ // An activity, or none without a type.
60
+ registry.function("activities.set", [types](Call call) {
61
+ @autoreleasepool {
62
+ const Value &options = call.options();
63
+ NSString *type = nil;
64
+ NSDictionary *userInfo = nil;
65
+ NSURL *page = nil;
66
+ if (!options["type"].isNull()) {
67
+ const std::string &name = options["type"].asString();
68
+ if (std::find(types.begin(), types.end(), name) == types.end()) {
69
+ throw std::invalid_argument("crossbind: app.setUserActivity takes a type app.activityTypes in crossbind.config.js lists; " + name + " is none");
70
+ }
71
+ type = stringOf(name);
72
+ const Value &info = options["userInfo"];
73
+ if (!info.isNull() && !info.isObject()) throw std::invalid_argument("crossbind: app.setUserActivity's userInfo is no object");
74
+ userInfo = info.isNull() ? @{} : plistOf(info);
75
+ const std::string address = options["webpageUrl"].asString("");
76
+ if (!address.empty()) {
77
+ page = [NSURL URLWithString:stringOf(address)];
78
+ // NSUserActivity takes an http or https page alone, and throws at another.
79
+ NSString *scheme = page.scheme.lowercaseString;
80
+ if (![scheme isEqualToString:@"https"] && ![scheme isEqualToString:@"http"]) {
81
+ throw std::invalid_argument("crossbind: app.setUserActivity's webpageUrl is no http or https address");
82
+ }
83
+ }
84
+ }
85
+ crossbind::onMainThread([call, type, userInfo, page] {
86
+ [current invalidate];
87
+ current = nil;
88
+ if (type) {
89
+ current = [[NSUserActivity alloc] initWithActivityType:type];
90
+ current.userInfo = userInfo;
91
+ current.webpageURL = page;
92
+ [current becomeCurrent];
93
+ }
94
+ call.resolve();
95
+ });
96
+ }
97
+ });
98
+ }
package/package.json CHANGED
@@ -1,7 +1,41 @@
1
1
  {
2
- "name": "@crossbind/extension-activities",
3
- "version": "0.0.0-bootstrap.0",
4
- "description": "Placeholder that let npm trust the crossbind release workflow; install @crossbind/extension-activities@beta.",
5
- "license": "MIT",
6
- "repository": "https://github.com/crossbind/crossbind.git"
7
- }
2
+ "name": "@crossbind/extension-activities",
3
+ "version": "2.0.0-beta.69",
4
+ "description": "crossbind app extension: Handoff on iOS and macOS, for the user activities the page sets and continues.",
5
+ "homepage": "https://github.com/crossbind/crossbind/tree/main/extensions/activities#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/crossbind/crossbind.git"
9
+ },
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "exports": {
13
+ "./page": {
14
+ "types": "./page.d.ts",
15
+ "default": "./page.js"
16
+ },
17
+ "./crossbind.extension.js": "./crossbind.extension.js"
18
+ },
19
+ "files": [
20
+ "crossbind.extension.js",
21
+ "native",
22
+ "page.d.ts",
23
+ "page.js"
24
+ ],
25
+ "crossbind": {
26
+ "extension": "./crossbind.extension.js"
27
+ },
28
+ "peerDependencies": {
29
+ "crossbind": "^2.0.0-beta.69"
30
+ },
31
+ "devDependencies": {
32
+ "crossbind": "^2.0.0-beta.69"
33
+ },
34
+ "keywords": [
35
+ "crossbind-extension",
36
+ "crossbind",
37
+ "launch",
38
+ "activities",
39
+ "app"
40
+ ]
41
+ }
package/page.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /** A user activity of an iOS or macOS app's, which another of its user's devices continues, as Handoff does: its type, one of app.activityTypes in crossbind.config.js, what the app keeps in its userInfo, JSON values, and the http or https page a device without the app opens instead. */
2
+ export interface UserActivity {
3
+ type: string;
4
+ /** An object, always, in the event user-activity. */
5
+ userInfo?: Record<string, unknown>;
6
+ webpageUrl?: string;
7
+ }
8
+
9
+ export declare const app: {
10
+ /** Makes this user activity the app's current one on iOS and macOS, for its user's other devices to continue, as Handoff does; null makes none current. Its type is one of app.activityTypes in crossbind.config.js, and another platform rejects. */
11
+ setUserActivity(activity: UserActivity | null): Promise<void>;
12
+ };
13
+
14
+ declare module 'crossbind/app/core' {
15
+ interface AppEvents {
16
+ /** Calls listener with each user activity of app.activityTypes the system continues an iOS or macOS app with once the page has called events.held('user-activity'), until the function it returns is called. */
17
+ 'user-activity': UserActivity;
18
+ }
19
+ interface HeldEvents {
20
+ /** The user activities of app.activityTypes the system continued the app with that the host held for the page, which came before it asked since it loaded, the launch's own among them. */
21
+ 'user-activity': UserActivity;
22
+ }
23
+ }
package/page.js ADDED
@@ -0,0 +1,30 @@
1
+ import { checkedOptions, invoke } from 'crossbind/app/core';
2
+
3
+ // A user activity is { type, userInfo, webpageUrl }: NSUserActivity takes an http or https page alone.
4
+ const ACTIVITY = { type: 'string', userInfo: { isFitting: (value) => value !== null && typeof value === 'object' && !Array.isArray(value), name: 'object' }, webpageUrl: 'string' };
5
+
6
+ function checkedActivity(activity) {
7
+ if (activity === null) return {};
8
+ checkedOptions('app.setUserActivity', activity, ACTIVITY);
9
+ if (!activity.type) throw new TypeError('crossbind: app.setUserActivity takes a type, one of app.activityTypes.');
10
+ const page = activity.webpageUrl;
11
+ if (page !== undefined && !(URL.canParse(page) && ['http:', 'https:'].includes(new URL(page).protocol))) {
12
+ throw new TypeError("crossbind: app.setUserActivity's webpageUrl is no http or https address.");
13
+ }
14
+ return activity;
15
+ }
16
+
17
+ const NOT_CURRENT = 'crossbind: an iOS or macOS app alone sets a user activity, of a type app.activityTypes in crossbind.config.js lists';
18
+
19
+ export const app = {
20
+ // Makes this user activity the app's current one on iOS and macOS, for its user's other devices to continue, as
21
+ // Handoff does: { type, userInfo, webpageUrl }, its type one of app.activityTypes. null makes none current.
22
+ setUserActivity: async (activity) => {
23
+ const checked = checkedActivity(activity);
24
+ try {
25
+ return await invoke('activities.set', checked);
26
+ } catch (error) {
27
+ throw error?.message === 'crossbind: an app has no function activities.set' ? new Error(NOT_CURRENT) : error;
28
+ }
29
+ },
30
+ };