@clearly-hub/browser 0.1.0 → 0.2.0
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/README.md +54 -34
- package/dist/clearly-hub.js +2 -2
- package/dist/index.d.ts +4 -69
- package/dist/index.js +82 -9
- package/dist/react.d.ts +24 -0
- package/dist/react.js +0 -0
- package/dist/screens-CZiYNLZI.d.ts +73 -0
- package/package.json +22 -3
package/README.md
CHANGED
|
@@ -11,13 +11,13 @@ hub) and each hub's subscription to your app. Tokens stay on your server.
|
|
|
11
11
|
|
|
12
12
|
**1. Register your app in Clearly.HUB** (on your app's page, no code):
|
|
13
13
|
|
|
14
|
-
| Setting
|
|
15
|
-
|
|
|
16
|
-
| Go to app
|
|
14
|
+
| Setting | Value |
|
|
15
|
+
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| Go to app | `https://app.example.com/clearly-hub/sign-in` |
|
|
17
17
|
| Sign-in client (H2M): callback URLs | `https://app.example.com/clearly-hub/callback`, `https://app.example.com/clearly-hub/answer`, `https://hub.clearly.app/components` |
|
|
18
|
-
| Sign-in client: sign-out URLs
|
|
19
|
-
| M2M client
|
|
20
|
-
| Plans
|
|
18
|
+
| Sign-in client: sign-out URLs | `https://app.example.com`, `https://hub.clearly.app/components` |
|
|
19
|
+
| M2M client | create it, keep the secret on your server |
|
|
20
|
+
| Plans | give each your own plan id as its _External reference_, for example `pro` |
|
|
21
21
|
|
|
22
22
|
**2. Mount the server handler** (Node 20+):
|
|
23
23
|
|
|
@@ -43,7 +43,8 @@ Hono, Bun or Deno. Mount it at `/clearly-hub/*`.
|
|
|
43
43
|
**3. Add the script and a button:**
|
|
44
44
|
|
|
45
45
|
```html
|
|
46
|
-
|
|
46
|
+
<!-- @0.1: follows 0.1.x fixes, not a breaking 0.2 -->
|
|
47
|
+
<script src="https://cdn.jsdelivr.net/npm/@clearly-hub/browser@0.1/dist/clearly-hub.js"></script>
|
|
47
48
|
|
|
48
49
|
<clearly-hub-button action="sign-in"></clearly-hub-button>
|
|
49
50
|
<clearly-hub-button action="DATA_PICKER"></clearly-hub-button>
|
|
@@ -54,18 +55,18 @@ Hono, Bun or Deno. Mount it at `/clearly-hub/*`.
|
|
|
54
55
|
</script>
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
Or `npm install @clearly-hub/browser` and `import { clearlyHub } from '@clearly-hub/browser'`.
|
|
58
|
+
Or `npm install @clearly-hub/browser` and `import { clearlyHub } from '@clearly-hub/browser'`. With React and TypeScript, add `import '@clearly-hub/browser/react'` once, so JSX knows `<clearly-hub-button>`.
|
|
58
59
|
|
|
59
60
|
## The browser package
|
|
60
61
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
| `clearlyHub.session()`
|
|
64
|
-
| `clearlyHub.signIn()`, `signOut()`
|
|
65
|
-
| `clearlyHub.open(action, { data, display })`
|
|
66
|
-
| `clearlyHub.result()`
|
|
67
|
-
| `<clearly-hub-button action display data label>` | Branded button for `sign-in`, `sign-out` and each screen
|
|
68
|
-
| `clearly-hub-answer` event
|
|
62
|
+
| | |
|
|
63
|
+
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| `clearlyHub.session()` | `{ signedIn: false }`, or the signed-in user (`sub`, `email`, `name`, `appUserId`) |
|
|
65
|
+
| `clearlyHub.signIn()`, `signOut()` | Go through Clearly.HUB and come back to this page |
|
|
66
|
+
| `clearlyHub.open(action, { data, display })` | Opens a hosted screen; resolves with its checked answer. Call it from a click: browsers only allow popups there |
|
|
67
|
+
| `clearlyHub.result()` | After a redirect screen: its answer, on the page it came back to |
|
|
68
|
+
| `<clearly-hub-button action display data label>` | Branded button for `sign-in`, `sign-out` and each screen |
|
|
69
|
+
| `clearly-hub-answer` event | Every answer, from a popup or after a redirect |
|
|
69
70
|
|
|
70
71
|
**Styling the button.** It uses your page's font. Attributes for the common cases:
|
|
71
72
|
`variant="outline"`, `size="small"` or `size="large"`. CSS custom properties for your look:
|
|
@@ -73,32 +74,51 @@ Or `npm install @clearly-hub/browser` and `import { clearlyHub } from '@clearly-
|
|
|
73
74
|
`--clearly-hub-font`. For anything else, style the real button through `::part(button)`:
|
|
74
75
|
|
|
75
76
|
```css
|
|
76
|
-
clearly-hub-button {
|
|
77
|
-
clearly-hub-
|
|
77
|
+
clearly-hub-button {
|
|
78
|
+
--clearly-hub-color: #0b7471;
|
|
79
|
+
--clearly-hub-radius: 999px;
|
|
80
|
+
}
|
|
81
|
+
clearly-hub-button::part(button) {
|
|
82
|
+
text-transform: uppercase;
|
|
83
|
+
}
|
|
78
84
|
```
|
|
79
85
|
|
|
80
86
|
Screens open in a popup by default (`data-display="redirect"` on the script tag changes that). A
|
|
81
87
|
blocked popup falls back to a redirect.
|
|
82
88
|
|
|
83
|
-
| Action
|
|
84
|
-
|
|
|
85
|
-
| `SELECT_SUBSCRIPTION` |
|
|
86
|
-
| `DATA_PICKER`
|
|
87
|
-
| `REGISTER_RESOURCE`
|
|
88
|
-
| `SELECT_HUB`
|
|
89
|
+
| Action | Send in `data` | `answer` when `result` is `SUCCESS` |
|
|
90
|
+
| --------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
91
|
+
| `SELECT_SUBSCRIPTION` | | `subscription` (`_id`, `status`, `externalReference`), `subscribedHub` (`_id`, `name`, `role`) |
|
|
92
|
+
| `DATA_PICKER` | | `activeHub`, `resources` (`url`, `format`, `accessibility`, `requiresToken`, `hasAccess`) |
|
|
93
|
+
| `REGISTER_RESOURCE` | `{ url, format }` of your result, at a public https URL | `activeHub`, `dataset`, `resource` |
|
|
94
|
+
| `SELECT_HUB` | | `hub` (`_id`, `name`) |
|
|
89
95
|
|
|
90
96
|
## The Node package
|
|
91
97
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
| `hub.express()`, `hub.handler`
|
|
95
|
-
| `onSignIn(person)`
|
|
96
|
-
| `onAnswer(result, { user })`
|
|
97
|
-
| `hub.getUser(req)`
|
|
98
|
-
| `hub.fetchAsUser(req, url)`
|
|
99
|
-
| `hub.getSubscription(hubId)`, `getSubscriptions()`
|
|
100
|
-
| `hub.reportUsage({ request, hubId, quantity })` | Report billable use; Clearly.HUB bills the hub |
|
|
101
|
-
| `hub.getUserToken(req)`, `getAppToken()`
|
|
98
|
+
| | |
|
|
99
|
+
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| `hub.express()`, `hub.handler` | The routes: `/sign-in`, `/callback`, `/sign-out`, `/session`, `/open`, `/answer`, `/result` |
|
|
101
|
+
| `onSignIn(person, { request, req })` | Called after each sign-in. Return your own user id to keep it in the session, or `{ appUserId, redirectTo }` to also choose the next page |
|
|
102
|
+
| `onAnswer(result, { user, data, request, req })` | Called with each checked answer before it reaches the browser, for example to set up a workspace. `data` is what the screen was sent; what you return reaches the page as `result.app` |
|
|
103
|
+
| `hub.getUser(req)` | Who's signed in, in your own routes |
|
|
104
|
+
| `hub.fetchAsUser(req, url)` | Fetch protected or paid data (from `DATA_PICKER`) with the person's key |
|
|
105
|
+
| `hub.getSubscription(hubId)`, `getSubscriptions()` | A hub's subscription to your app, or all of them |
|
|
106
|
+
| `hub.reportUsage({ request or sub, hubId, quantity })` | Report billable use; Clearly.HUB bills the hub. Pass `sub` for a Clearly.HUB account you stored, without a live sign-in |
|
|
107
|
+
| `hub.getUserToken(req)`, `getAppToken()` | The person's key and your app's key, for calls of your own |
|
|
108
|
+
|
|
109
|
+
**Your own session in the hooks.** With `hub.express()`, the hooks get Express's `req` as
|
|
110
|
+
`context.req`, so they can use your own session (for example express-session) alongside Clearly.HUB's:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const hub = clearlyHub<express.Request>({
|
|
114
|
+
// ...
|
|
115
|
+
onSignIn: (person, { req }) => {
|
|
116
|
+
const user = findOrCreateUser(person);
|
|
117
|
+
req!.session.userId = user.id;
|
|
118
|
+
return { appUserId: user.id };
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
```
|
|
102
122
|
|
|
103
123
|
**What it checks for you:** every screen answer's Ed25519 signature, and that it answers a screen
|
|
104
124
|
opened in this browser, once (`state`). The session is encrypted in cookies, so there's nothing to
|
package/dist/clearly-hub.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";(()=>{var
|
|
1
|
+
"use strict";(()=>{var C=Object.defineProperty;var v=(r,t,n)=>t in r?C(r,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[t]=n;var l=(r,t,n)=>v(r,typeof t!="symbol"?t+"":t,n);var x=["SELECT_SUBSCRIPTION","DATA_PICKER","REGISTER_RESOURCE","SELECT_HUB"];function g(r){return x.includes(r)}function S(r,t){if(r!=="REGISTER_RESOURCE")return;let{url:n,format:i}=t??{};if(typeof n!="string"||!n||typeof i!="string"||!i)return'REGISTER_RESOURCE needs data with a url and a format, for example { url, format: "geojson" }';let o;try{o=new URL(n).hostname}catch{return`REGISTER_RESOURCE: "${n}" is not a URL`}if(!o.includes("."))return"REGISTER_RESOURCE needs a url with a domain Clearly.HUB accepts, not localhost"}var E="/clearly-hub";var c={signIn:"/sign-in",callback:"/callback",signOut:"/sign-out",session:"/session",open:"/open",answer:"/answer",result:"/result"},b="clearly-hub-answers";var d="clearly-hub-answer",y=480,h=class{constructor(t={}){l(this,"basePath",E);l(this,"display","popup");l(this,"redirectResult");this.configure(t)}configure(t){return t.basePath&&(this.basePath=t.basePath.replace(/\/+$/,"")),t.display&&(this.display=t.display),this}async session(){let t=await fetch(this.url(c.session),{credentials:"same-origin"});if(!t.ok)throw new Error(`Clearly.HUB session failed (${t.status})`);return t.json()}signIn({returnTo:t=R()}={}){window.location.assign(this.url(c.signIn,{returnTo:t}))}signOut(){window.location.assign(this.url(c.signOut))}open(t,n={}){let i=S(t,n.data);if(i)return Promise.reject(new Error(`Clearly.HUB: ${i}`));let o=crypto.randomUUID(),e=n.display??this.display,s={action:t,ref:o,returnTo:n.returnTo??R(),data:n.data?JSON.stringify(n.data):void 0};if(e==="popup"){let a=window.open(this.url(c.open,{...s,display:"popup"}),"clearly-hub-screen",`width=${y},height=${screen.availHeight},left=${screen.availWidth-y}`);if(a)return this.waitForPopup(a,o,t)}return window.location.assign(this.url(c.open,{...s,display:"redirect"})),new Promise(()=>{})}result(){return this.redirectResult??(this.redirectResult=fetch(this.url(c.result),{credentials:"same-origin"}).then(t=>t.ok?t.json():null).then(t=>t?.result??null).catch(()=>null)),this.redirectResult}waitForPopup(t,n,i){return new Promise(o=>{let e=new BroadcastChannel(b),s=!1,a=u=>{s||(s=!0,e.close(),window.clearInterval(f),window.dispatchEvent(new CustomEvent(d,{detail:u})),o(u))};e.onmessage=u=>{u.data?.ref===n&&a(u.data.result)};let f=window.setInterval(()=>{t.closed&&(window.clearInterval(f),window.setTimeout(()=>a({action:i,result:"CANCELLED"}),500))},400)})}url(t,n={}){let i=new URLSearchParams;for(let[e,s]of Object.entries(n))s!==void 0&&i.set(e,s);let o=i.toString();return`${this.basePath}${t}${o?`?${o}`:""}`}};function R(){return`${window.location.pathname}${window.location.search}${window.location.hash}`}var m={"sign-in":"Sign in with Clearly.HUB","sign-out":"Sign out",SELECT_SUBSCRIPTION:"Choose a plan in Clearly.HUB",DATA_PICKER:"Pick data from Clearly.HUB",REGISTER_RESOURCE:"Save to Clearly.HUB",SELECT_HUB:"Choose a hub"},T=`
|
|
2
2
|
:host { display: inline-block; }
|
|
3
3
|
button {
|
|
4
4
|
--color: var(--clearly-hub-color, #1fa69d);
|
|
@@ -20,4 +20,4 @@
|
|
|
20
20
|
button:focus-visible { outline: 2px solid var(--color); outline-offset: 2px; }
|
|
21
21
|
button:disabled { opacity: 0.6; cursor: default; }
|
|
22
22
|
svg { width: 1.25em; height: 1.25em; flex: none; fill: currentColor; }
|
|
23
|
-
`,
|
|
23
|
+
`,O='<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 6.5c-1.66 0-3 1.34-3 3 0 .07 0 .14.01.21l-2.03.68c-.64-1.21-1.82-2.09-3.22-2.32V5.91C14.04 5.57 15 4.4 15 3c0-1.66-1.34-3-3-3S9 1.34 9 3c0 1.4.96 2.57 2.25 2.91v2.16c-1.4.23-2.58 1.11-3.22 2.32l-2.04-.68C6 9.64 6 9.57 6 9.5c0-1.66-1.34-3-3-3s-3 1.34-3 3 1.34 3 3 3c1.06 0 1.98-.55 2.52-1.37l2.03.68c-.2 1.29.17 2.66 1.09 3.69l-1.41 1.77Q6.66 17 6 17c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3c0-.68-.22-1.3-.6-1.8l1.41-1.77c1.36.76 3.02.75 4.37 0l1.41 1.77c-.37.5-.59 1.12-.59 1.8 0 1.66 1.34 3 3 3s3-1.34 3-3-1.34-3-3-3c-.44 0-.85.09-1.23.26l-1.41-1.77c.93-1.04 1.29-2.4 1.09-3.69l2.03-.68c.53.82 1.46 1.37 2.52 1.37 1.66 0 3-1.34 3-3S22.66 6.5 21 6.5m-18 4c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1M6 21c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m5-18c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1-1-.45-1-1m1 12c-1.38 0-2.5-1.12-2.5-2.5S10.62 10 12 10s2.5 1.12 2.5 2.5S13.38 15 12 15m6 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1m3-8.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1"/></svg>';function A(r,t="clearly-hub-button"){if(typeof customElements>"u"||customElements.get(t))return;class n extends HTMLElement{constructor(){super();l(this,"button");let e=this.attachShadow({mode:"open"});e.innerHTML=`<style>${T}</style><button part="button" type="button"></button>`,this.button=e.querySelector("button"),this.button.addEventListener("click",()=>this.run())}connectedCallback(){for(let e of["action","display","label","variant","size","data"])if(Object.prototype.hasOwnProperty.call(this,e)){let s=this[e];delete this[e],this[e]=s}this.render()}attributeChangedCallback(){this.render()}get action(){let e=this.getAttribute("action")??"sign-in";return e in m?e:"sign-in"}set action(e){this.setOrRemove("action",e)}get display(){return this.getAttribute("display")==="redirect"?"redirect":void 0}set display(e){this.setOrRemove("display",e)}get label(){return this.getAttribute("label")}set label(e){this.setOrRemove("label",e)}get variant(){return this.getAttribute("variant")}set variant(e){this.setOrRemove("variant",e)}get size(){return this.getAttribute("size")}set size(e){this.setOrRemove("size",e)}get data(){let e=this.getAttribute("data");return e?JSON.parse(e):void 0}set data(e){this.setOrRemove("data",typeof e=="string"||e==null?e:JSON.stringify(e))}setOrRemove(e,s){s==null?this.removeAttribute(e):this.setAttribute(e,s)}render(){let e=this.label??m[this.action];this.button.innerHTML=`${O}<span></span>`,this.button.querySelector("span").textContent=e}async run(){let e=this.action;if(e==="sign-in")return r.signIn();if(e==="sign-out")return r.signOut();if(!g(e))return;this.button.disabled=!0;let s;try{s=await r.open(e,{display:this.display,data:this.data})}catch(a){console.error(a),s={action:e,result:"FAILURE",error:a.message}}finally{this.button.disabled=!1}this.dispatchEvent(new CustomEvent(d,{detail:s,bubbles:!0,composed:!0}))}}l(n,"observedAttributes",["action","label"]),customElements.define(t,n)}var p=new h;A(p);typeof window<"u"&&queueMicrotask(()=>{p.result().then(r=>{r&&window.dispatchEvent(new CustomEvent(d,{detail:r}))})});var w=document.currentScript;p.configure({basePath:w?.dataset.basePath,display:w?.dataset.display==="redirect"?"redirect":void 0});window.clearlyHub=p;})();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,70 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
declare function isScreenAction(value: unknown): value is ScreenAction;
|
|
4
|
-
type ScreenDisplay = 'redirect' | 'popup';
|
|
5
|
-
interface ScreenData {
|
|
6
|
-
REGISTER_RESOURCE: {
|
|
7
|
-
url: string;
|
|
8
|
-
format: string;
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
interface HubRef {
|
|
12
|
-
_id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
}
|
|
15
|
-
interface PickedResource {
|
|
16
|
-
datasetTitle: string;
|
|
17
|
-
resourceName: string;
|
|
18
|
-
format: string;
|
|
19
|
-
accessibility: 'OPEN' | 'PROTECTED' | 'PAID';
|
|
20
|
-
url: string;
|
|
21
|
-
requiresToken: boolean;
|
|
22
|
-
hasAccess: boolean;
|
|
23
|
-
}
|
|
24
|
-
interface ScreenAnswers {
|
|
25
|
-
SELECT_SUBSCRIPTION: {
|
|
26
|
-
subscription: {
|
|
27
|
-
_id: string;
|
|
28
|
-
status: 'ACTIVE' | 'PENDING';
|
|
29
|
-
externalReference: string;
|
|
30
|
-
};
|
|
31
|
-
subscribedHub: HubRef & {
|
|
32
|
-
role?: 'ADMIN' | 'MEMBER';
|
|
33
|
-
};
|
|
34
|
-
user?: {
|
|
35
|
-
_id: string;
|
|
36
|
-
email: string;
|
|
37
|
-
};
|
|
38
|
-
hasMultipleSubscriptions?: boolean;
|
|
39
|
-
};
|
|
40
|
-
DATA_PICKER: {
|
|
41
|
-
activeHub: HubRef;
|
|
42
|
-
resources: PickedResource[];
|
|
43
|
-
};
|
|
44
|
-
REGISTER_RESOURCE: {
|
|
45
|
-
activeHub: string;
|
|
46
|
-
dataset: {
|
|
47
|
-
datasetId: string;
|
|
48
|
-
datasetTitle?: string;
|
|
49
|
-
};
|
|
50
|
-
resource: {
|
|
51
|
-
resourceId: string;
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
SELECT_HUB: {
|
|
55
|
-
hub: HubRef;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
type ScreenResult<A extends ScreenAction = ScreenAction> = {
|
|
59
|
-
action: A;
|
|
60
|
-
result: 'SUCCESS';
|
|
61
|
-
answer: ScreenAnswers[A];
|
|
62
|
-
} | {
|
|
63
|
-
action: A;
|
|
64
|
-
result: 'CANCELLED' | 'FAILURE';
|
|
65
|
-
answer?: undefined;
|
|
66
|
-
error?: string;
|
|
67
|
-
};
|
|
1
|
+
import { S as ScreenResult, a as ScreenDisplay, b as ScreenAction, c as ScreenData } from './screens-CZiYNLZI.js';
|
|
2
|
+
export { H as HubRef, P as PickedResource, d as SCREEN_ACTIONS, e as ScreenAnswers, f as checkScreenData, i as isScreenAction } from './screens-CZiYNLZI.js';
|
|
68
3
|
|
|
69
4
|
interface SessionUser {
|
|
70
5
|
sub: string;
|
|
@@ -135,5 +70,5 @@ declare function defineButton(client: ClearlyHubClient, tagName?: string): void;
|
|
|
135
70
|
|
|
136
71
|
declare const clearlyHub: ClearlyHubClient;
|
|
137
72
|
|
|
138
|
-
export { ANSWER_CHANNEL, ANSWER_EVENT, ClearlyHubClient, DEFAULT_AUTH_URL, DEFAULT_BASE_PATH, DEFAULT_HUB_URL, DEFAULT_ISSUER, ROUTES,
|
|
139
|
-
export type { AnswerMessage, ClientOptions,
|
|
73
|
+
export { ANSWER_CHANNEL, ANSWER_EVENT, ClearlyHubClient, DEFAULT_AUTH_URL, DEFAULT_BASE_PATH, DEFAULT_HUB_URL, DEFAULT_ISSUER, ROUTES, ScreenAction, ScreenData, ScreenDisplay, ScreenResult, clearlyHub, defineButton };
|
|
74
|
+
export type { AnswerMessage, ClientOptions, HubSubscription, OpenOptions, Session, SessionUser };
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,23 @@ var SCREEN_ACTIONS = [
|
|
|
12
12
|
function isScreenAction(value) {
|
|
13
13
|
return SCREEN_ACTIONS.includes(value);
|
|
14
14
|
}
|
|
15
|
+
function checkScreenData(action, data) {
|
|
16
|
+
if (action !== "REGISTER_RESOURCE") return void 0;
|
|
17
|
+
const { url, format } = data ?? {};
|
|
18
|
+
if (typeof url !== "string" || !url || typeof format !== "string" || !format) {
|
|
19
|
+
return 'REGISTER_RESOURCE needs data with a url and a format, for example { url, format: "geojson" }';
|
|
20
|
+
}
|
|
21
|
+
let hostname;
|
|
22
|
+
try {
|
|
23
|
+
hostname = new URL(url).hostname;
|
|
24
|
+
} catch {
|
|
25
|
+
return `REGISTER_RESOURCE: "${url}" is not a URL`;
|
|
26
|
+
}
|
|
27
|
+
if (!hostname.includes(".")) {
|
|
28
|
+
return "REGISTER_RESOURCE needs a url with a domain Clearly.HUB accepts, not localhost";
|
|
29
|
+
}
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
15
32
|
|
|
16
33
|
// ../shared/src/protocol.ts
|
|
17
34
|
var DEFAULT_BASE_PATH = "/clearly-hub";
|
|
@@ -57,6 +74,8 @@ var ClearlyHubClient = class {
|
|
|
57
74
|
// click handler: browsers only allow popups there. A blocked popup falls
|
|
58
75
|
// back to a redirect.
|
|
59
76
|
open(action, options = {}) {
|
|
77
|
+
const dataProblem = checkScreenData(action, options.data);
|
|
78
|
+
if (dataProblem) return Promise.reject(new Error(`Clearly.HUB: ${dataProblem}`));
|
|
60
79
|
const ref = crypto.randomUUID();
|
|
61
80
|
const display = options.display ?? this.display;
|
|
62
81
|
const query = {
|
|
@@ -163,17 +182,69 @@ function defineButton(client, tagName = "clearly-hub-button") {
|
|
|
163
182
|
this.button.addEventListener("click", () => this.run());
|
|
164
183
|
}
|
|
165
184
|
connectedCallback() {
|
|
185
|
+
for (const name of ["action", "display", "label", "variant", "size", "data"]) {
|
|
186
|
+
if (Object.prototype.hasOwnProperty.call(this, name)) {
|
|
187
|
+
const value = this[name];
|
|
188
|
+
delete this[name];
|
|
189
|
+
this[name] = value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
166
192
|
this.render();
|
|
167
193
|
}
|
|
168
194
|
attributeChangedCallback() {
|
|
169
195
|
this.render();
|
|
170
196
|
}
|
|
197
|
+
// Every attribute is also a property that writes the attribute, as web
|
|
198
|
+
// components do. React 19, for one, sets props as properties when the
|
|
199
|
+
// element has them.
|
|
171
200
|
get action() {
|
|
172
201
|
const action = this.getAttribute("action") ?? "sign-in";
|
|
173
202
|
return action in LABELS ? action : "sign-in";
|
|
174
203
|
}
|
|
204
|
+
set action(value) {
|
|
205
|
+
this.setOrRemove("action", value);
|
|
206
|
+
}
|
|
207
|
+
get display() {
|
|
208
|
+
return this.getAttribute("display") === "redirect" ? "redirect" : void 0;
|
|
209
|
+
}
|
|
210
|
+
set display(value) {
|
|
211
|
+
this.setOrRemove("display", value);
|
|
212
|
+
}
|
|
213
|
+
get label() {
|
|
214
|
+
return this.getAttribute("label");
|
|
215
|
+
}
|
|
216
|
+
set label(value) {
|
|
217
|
+
this.setOrRemove("label", value);
|
|
218
|
+
}
|
|
219
|
+
get variant() {
|
|
220
|
+
return this.getAttribute("variant");
|
|
221
|
+
}
|
|
222
|
+
set variant(value) {
|
|
223
|
+
this.setOrRemove("variant", value);
|
|
224
|
+
}
|
|
225
|
+
get size() {
|
|
226
|
+
return this.getAttribute("size");
|
|
227
|
+
}
|
|
228
|
+
set size(value) {
|
|
229
|
+
this.setOrRemove("size", value);
|
|
230
|
+
}
|
|
231
|
+
// As an attribute: JSON text. As a property: also an object
|
|
232
|
+
get data() {
|
|
233
|
+
const data = this.getAttribute("data");
|
|
234
|
+
return data ? JSON.parse(data) : void 0;
|
|
235
|
+
}
|
|
236
|
+
set data(value) {
|
|
237
|
+
this.setOrRemove(
|
|
238
|
+
"data",
|
|
239
|
+
typeof value === "string" || value == null ? value : JSON.stringify(value)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
setOrRemove(name, value) {
|
|
243
|
+
if (value == null) this.removeAttribute(name);
|
|
244
|
+
else this.setAttribute(name, value);
|
|
245
|
+
}
|
|
175
246
|
render() {
|
|
176
|
-
const label = this.
|
|
247
|
+
const label = this.label ?? LABELS[this.action];
|
|
177
248
|
this.button.innerHTML = `${ICON}<span></span>`;
|
|
178
249
|
this.button.querySelector("span").textContent = label;
|
|
179
250
|
}
|
|
@@ -182,20 +253,22 @@ function defineButton(client, tagName = "clearly-hub-button") {
|
|
|
182
253
|
if (action === "sign-in") return client.signIn();
|
|
183
254
|
if (action === "sign-out") return client.signOut();
|
|
184
255
|
if (!isScreenAction(action)) return;
|
|
185
|
-
const data = this.getAttribute("data");
|
|
186
|
-
const display = this.getAttribute("display") === "redirect" ? "redirect" : void 0;
|
|
187
256
|
this.button.disabled = true;
|
|
257
|
+
let result;
|
|
188
258
|
try {
|
|
189
|
-
|
|
190
|
-
display,
|
|
191
|
-
data:
|
|
259
|
+
result = await client.open(action, {
|
|
260
|
+
display: this.display,
|
|
261
|
+
data: this.data
|
|
192
262
|
});
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(error);
|
|
265
|
+
result = { action, result: "FAILURE", error: error.message };
|
|
196
266
|
} finally {
|
|
197
267
|
this.button.disabled = false;
|
|
198
268
|
}
|
|
269
|
+
this.dispatchEvent(
|
|
270
|
+
new CustomEvent(ANSWER_EVENT, { detail: result, bubbles: true, composed: true })
|
|
271
|
+
);
|
|
199
272
|
}
|
|
200
273
|
}
|
|
201
274
|
__publicField(ClearlyHubButton, "observedAttributes", ["action", "label"]);
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
2
|
+
import { ScreenAction, ScreenDisplay } from './index.js';
|
|
3
|
+
|
|
4
|
+
interface ClearlyHubButtonAttributes {
|
|
5
|
+
action?: ScreenAction | 'sign-in' | 'sign-out';
|
|
6
|
+
display?: ScreenDisplay;
|
|
7
|
+
data?: string | {
|
|
8
|
+
url: string;
|
|
9
|
+
format: string;
|
|
10
|
+
};
|
|
11
|
+
label?: string;
|
|
12
|
+
variant?: 'solid' | 'outline';
|
|
13
|
+
size?: 'small' | 'large';
|
|
14
|
+
}
|
|
15
|
+
type ClearlyHubButtonProps = DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & ClearlyHubButtonAttributes;
|
|
16
|
+
declare module 'react' {
|
|
17
|
+
namespace JSX {
|
|
18
|
+
interface IntrinsicElements {
|
|
19
|
+
'clearly-hub-button': ClearlyHubButtonProps;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type { ClearlyHubButtonAttributes };
|
package/dist/react.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
declare const SCREEN_ACTIONS: readonly ["SELECT_SUBSCRIPTION", "DATA_PICKER", "REGISTER_RESOURCE", "SELECT_HUB"];
|
|
2
|
+
type ScreenAction = (typeof SCREEN_ACTIONS)[number];
|
|
3
|
+
declare function isScreenAction(value: unknown): value is ScreenAction;
|
|
4
|
+
type ScreenDisplay = 'redirect' | 'popup';
|
|
5
|
+
interface ScreenData {
|
|
6
|
+
REGISTER_RESOURCE: {
|
|
7
|
+
url: string;
|
|
8
|
+
format: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
declare function checkScreenData(action: ScreenAction, data: unknown): string | undefined;
|
|
12
|
+
interface HubRef {
|
|
13
|
+
_id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
interface PickedResource {
|
|
17
|
+
datasetTitle: string;
|
|
18
|
+
resourceName: string;
|
|
19
|
+
format: string;
|
|
20
|
+
accessibility: 'OPEN' | 'PROTECTED' | 'PAID';
|
|
21
|
+
url: string;
|
|
22
|
+
requiresToken: boolean;
|
|
23
|
+
hasAccess: boolean;
|
|
24
|
+
}
|
|
25
|
+
interface ScreenAnswers {
|
|
26
|
+
SELECT_SUBSCRIPTION: {
|
|
27
|
+
subscription: {
|
|
28
|
+
_id: string;
|
|
29
|
+
status: 'ACTIVE' | 'PENDING';
|
|
30
|
+
externalReference: string;
|
|
31
|
+
};
|
|
32
|
+
subscribedHub: HubRef & {
|
|
33
|
+
role?: 'ADMIN' | 'MEMBER';
|
|
34
|
+
};
|
|
35
|
+
user?: {
|
|
36
|
+
_id: string;
|
|
37
|
+
email: string;
|
|
38
|
+
};
|
|
39
|
+
hasMultipleSubscriptions?: boolean;
|
|
40
|
+
};
|
|
41
|
+
DATA_PICKER: {
|
|
42
|
+
activeHub: HubRef;
|
|
43
|
+
resources: PickedResource[];
|
|
44
|
+
};
|
|
45
|
+
REGISTER_RESOURCE: {
|
|
46
|
+
activeHub: string;
|
|
47
|
+
dataset: {
|
|
48
|
+
datasetId: string;
|
|
49
|
+
datasetTitle?: string;
|
|
50
|
+
};
|
|
51
|
+
resource: {
|
|
52
|
+
resourceId: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
SELECT_HUB: {
|
|
56
|
+
hub: HubRef;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
type ScreenResult<A extends ScreenAction = ScreenAction> = {
|
|
60
|
+
action: A;
|
|
61
|
+
result: 'SUCCESS';
|
|
62
|
+
answer: ScreenAnswers[A];
|
|
63
|
+
app?: unknown;
|
|
64
|
+
} | {
|
|
65
|
+
action: A;
|
|
66
|
+
result: 'CANCELLED' | 'FAILURE';
|
|
67
|
+
answer?: undefined;
|
|
68
|
+
error?: string;
|
|
69
|
+
app?: unknown;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export { SCREEN_ACTIONS as d, checkScreenData as f, isScreenAction as i };
|
|
73
|
+
export type { HubRef as H, PickedResource as P, ScreenResult as S, ScreenDisplay as a, ScreenAction as b, ScreenData as c, ScreenAnswers as e };
|
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clearly-hub/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Clearly.HUB in the browser: sign-in, hosted screens and the session, talking to @clearly-hub/node",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Future Insight Group BV",
|
|
7
|
-
"keywords": [
|
|
7
|
+
"keywords": [
|
|
8
|
+
"clearly-hub",
|
|
9
|
+
"oup",
|
|
10
|
+
"open-urban-platform",
|
|
11
|
+
"web-component",
|
|
12
|
+
"sdk"
|
|
13
|
+
],
|
|
8
14
|
"type": "module",
|
|
9
15
|
"module": "./dist/index.js",
|
|
10
16
|
"types": "./dist/index.d.ts",
|
|
@@ -13,6 +19,10 @@
|
|
|
13
19
|
"types": "./dist/index.d.ts",
|
|
14
20
|
"import": "./dist/index.js"
|
|
15
21
|
},
|
|
22
|
+
"./react": {
|
|
23
|
+
"types": "./dist/react.d.ts",
|
|
24
|
+
"import": "./dist/react.js"
|
|
25
|
+
},
|
|
16
26
|
"./clearly-hub.js": "./dist/clearly-hub.js"
|
|
17
27
|
},
|
|
18
28
|
"unpkg": "./dist/clearly-hub.js",
|
|
@@ -30,7 +40,16 @@
|
|
|
30
40
|
"typecheck": "tsc -p .",
|
|
31
41
|
"prepack": "node ../../scripts/copy-license.mjs && npm run build"
|
|
32
42
|
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@types/react": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@types/react": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
33
51
|
"devDependencies": {
|
|
34
|
-
"@clearly-hub/shared": "
|
|
52
|
+
"@clearly-hub/shared": "*",
|
|
53
|
+
"@types/react": "^19.3.0"
|
|
35
54
|
}
|
|
36
55
|
}
|