@clearly-hub/browser 0.2.1 → 0.4.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 +39 -13
- package/dist/clearly-hub.js +2 -2
- package/dist/index.d.ts +30 -19
- package/dist/index.js +122 -28
- package/dist/react.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Connect your web app to Clearly.HUB with a script, some config and one server handler. You get
|
|
4
4
|
"Sign in with Clearly.HUB", the hosted screens (choose a plan, pick data, save a result, choose a
|
|
5
|
-
hub) and each hub's subscription to your app.
|
|
5
|
+
hub) and each hub's subscription to your app. The refresh token stays on your server; the page gets
|
|
6
|
+
the access token only when it asks, for protected data.
|
|
6
7
|
|
|
7
8
|
> **Preview (0.x).** The API may still change between minor versions. Proprietary: see
|
|
8
9
|
> [LICENSE](LICENSE).
|
|
@@ -51,7 +52,9 @@ Hono, Bun or Deno. Mount it at `/clearly-hub/*`.
|
|
|
51
52
|
|
|
52
53
|
<script type="module">
|
|
53
54
|
const session = await clearlyHub.session(); // { signedIn, user }
|
|
54
|
-
window.addEventListener('clearly-hub-answer', (event) =>
|
|
55
|
+
window.addEventListener('clearly-hub-answer', (event) =>
|
|
56
|
+
console.log(event.detail),
|
|
57
|
+
);
|
|
55
58
|
</script>
|
|
56
59
|
```
|
|
57
60
|
|
|
@@ -59,14 +62,16 @@ Or `npm install @clearly-hub/browser` and `import { clearlyHub } from '@clearly-
|
|
|
59
62
|
|
|
60
63
|
## The browser package
|
|
61
64
|
|
|
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
|
-
|
|
|
69
|
-
| `
|
|
65
|
+
| | |
|
|
66
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| `clearlyHub.session()` | `{ signedIn: false }`, or the signed-in user (`sub`, `email`, `name`, `appUserId`) |
|
|
68
|
+
| `clearlyHub.signIn()`, `signOut()` | Go through Clearly.HUB and come back to this page |
|
|
69
|
+
| `clearlyHub.open(action, { data, display })` | Opens a hosted screen; resolves with its checked answer. Call it from a click: browsers only allow popups there |
|
|
70
|
+
| `clearlyHub.result()` | After a redirect screen: its answer, on the page it came back to |
|
|
71
|
+
| `clearlyHub.getToken()` | The person's key for protected and paid data, or `null` when signed out. See [Protected data](#protected-data) |
|
|
72
|
+
| `clearlyHub.onAnswer(callback)` | Every answer, once each, from a popup or after a redirect; also when you start listening after it arrived (React). Returns a function that stops listening |
|
|
73
|
+
| `<clearly-hub-button action display data label>` | Branded button for `sign-in`, `sign-out` and each screen |
|
|
74
|
+
| `clearly-hub-answer` event | Every answer, from a popup or after a redirect |
|
|
70
75
|
|
|
71
76
|
**Styling the button.** It uses your page's font. Attributes for the common cases:
|
|
72
77
|
`variant="outline"`, `size="small"` or `size="large"`. CSS custom properties for your look:
|
|
@@ -93,15 +98,33 @@ blocked popup falls back to a redirect.
|
|
|
93
98
|
| `REGISTER_RESOURCE` | `{ url, format }` of your result, at a public https URL | `activeHub`, `dataset`, `resource` |
|
|
94
99
|
| `SELECT_HUB` | | `hub` (`_id`, `name`) |
|
|
95
100
|
|
|
101
|
+
## Protected data
|
|
102
|
+
|
|
103
|
+
A protected or paid resource from `DATA_PICKER` (`requiresToken: true`) has a `url` on Clearly.HUB,
|
|
104
|
+
which checks the person's key on every request. Send it as the `Authorization` header. In the page:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
const response = await fetch(resource.url, {
|
|
108
|
+
headers: { Authorization: await clearlyHub.getToken() },
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Map layers (WMS, WMTS, tiles) load many URLs themselves: use a Leaflet, OpenLayers or Cesium option
|
|
113
|
+
or plugin that adds headers to each request. The key lasts an hour; `getToken()` gets a new one in
|
|
114
|
+
time, so call it again rather than keeping the value. On your server, `hub.fetchAsUser(req, url)`
|
|
115
|
+
does the same.
|
|
116
|
+
|
|
117
|
+
The key is the person's own Clearly.HUB key: keep scripts you don't trust off pages that use it.
|
|
118
|
+
|
|
96
119
|
## The Node package
|
|
97
120
|
|
|
98
121
|
| | |
|
|
99
122
|
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
-
| `hub.express()`, `hub.handler` | The routes: `/sign-in`, `/callback`, `/sign-out`, `/session`, `/open`, `/answer`, `/result`
|
|
123
|
+
| `hub.express()`, `hub.handler` | The routes: `/sign-in`, `/callback`, `/sign-out`, `/session`, `/open`, `/answer`, `/result`, `/token` |
|
|
101
124
|
| `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
125
|
| `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
126
|
| `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
|
|
127
|
+
| `hub.fetchAsUser(req, url)` | Fetch protected or paid data (from `DATA_PICKER`) on your server, with the person's key |
|
|
105
128
|
| `hub.getSubscription(hubId)`, `getSubscriptions()` | A hub's subscription to your app, or all of them |
|
|
106
129
|
| `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
130
|
| `hub.getUserToken(req)`, `getAppToken()` | The person's key and your app's key, for calls of your own |
|
|
@@ -131,7 +154,10 @@ store; it's split over several cookies because tokens are large.
|
|
|
131
154
|
|
|
132
155
|
- Setting up your own workspaces or tenants: do it by hand, with plans that need approval, or in
|
|
133
156
|
`onAnswer` for `SELECT_SUBSCRIPTION`.
|
|
134
|
-
- Other server languages:
|
|
157
|
+
- Other server languages (.NET, Python, ...): the browser package works with any
|
|
158
|
+
backend that answers the same routes. Contact us at
|
|
159
|
+
[support@futureinsight.nl](mailto:support@futureinsight.nl) and we'll help
|
|
160
|
+
you with yours.
|
|
135
161
|
|
|
136
162
|
## Working on this repo
|
|
137
163
|
|
package/dist/clearly-hub.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";(()=>{var C=Object.defineProperty;var T=(r,
|
|
1
|
+
"use strict";(()=>{var C=Object.defineProperty;var T=(r,e,n)=>e in r?C(r,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[e]=n;var a=(r,e,n)=>T(r,typeof e!="symbol"?e+"":e,n);var E="/clearly-hub";var l={signIn:"/sign-in",callback:"/callback",signOut:"/sign-out",session:"/session",open:"/open",answer:"/answer",result:"/result",token:"/token"},R="clearly-hub-answers";var x=["SELECT_SUBSCRIPTION","DATA_PICKER","REGISTER_RESOURCE","SELECT_HUB"];function S(r){return x.includes(r)}function b(r,e){if(r!=="REGISTER_RESOURCE")return;let{url:n,format:s}=e??{};if(typeof n!="string"||!n||typeof s!="string"||!s)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 u="clearly-hub-answer",y=480,h=class{constructor(e={}){a(this,"basePath",E);a(this,"display","popup");a(this,"redirectResult");a(this,"unclaimedRedirectResult");a(this,"token");a(this,"tokenRequest");this.configure(e)}configure(e){return e.basePath&&(this.basePath=e.basePath.replace(/\/+$/,"")),e.display&&(this.display=e.display),this}async session(){let e=await fetch(this.url(l.session),{credentials:"same-origin"});if(!e.ok)throw new Error(`Clearly.HUB session failed (${e.status})`);return e.json()}async getToken(){return this.token&&this.token.expiresAt>Date.now()+6e4?this.token.value:(this.tokenRequest??(this.tokenRequest=fetch(this.url(l.token),{credentials:"same-origin"}).then(async e=>{if(e.status===401)return null;if(!e.ok)throw new Error(`Clearly.HUB token failed (${e.status})`);let{accessToken:n,expiresIn:s}=await e.json();return{value:n,expiresAt:Date.now()+s*1e3}}).finally(()=>{this.tokenRequest=void 0})),this.token=await this.tokenRequest??void 0,this.token?.value??null)}signIn({returnTo:e=m()}={}){window.location.assign(this.url(l.signIn,{returnTo:e}))}signOut(){window.location.assign(this.url(l.signOut))}open(e,n={}){let s=b(e,n.data);if(s)return Promise.reject(new Error(`Clearly.HUB: ${s}`));let o=crypto.randomUUID(),t=n.display??this.display,i={action:e,ref:o,returnTo:n.returnTo??m(),data:n.data?JSON.stringify(n.data):void 0};if(t==="popup"){let c=window.open(this.url(l.open,{...i,display:"popup"}),"clearly-hub-screen",`width=${y},height=${screen.availHeight},left=${screen.availWidth-y}`);if(c)return this.waitForPopup(c,o,e,n.eventTarget??window)}return window.location.assign(this.url(l.open,{...i,display:"redirect"})),new Promise(()=>{})}onAnswer(e){let n=o=>{let t=o.detail;t===this.unclaimedRedirectResult&&(this.unclaimedRedirectResult=void 0),e(t)};window.addEventListener(u,n);let s=this.unclaimedRedirectResult;return s&&(this.unclaimedRedirectResult=void 0,e(s)),()=>window.removeEventListener(u,n)}deliverRedirectResult(e){this.unclaimedRedirectResult=e,window.dispatchEvent(new CustomEvent(u,{detail:e}))}result(){return this.redirectResult??(this.redirectResult=fetch(this.url(l.result),{credentials:"same-origin"}).then(e=>e.ok?e.json():null).then(e=>e?.result??null).catch(()=>null)),this.redirectResult}waitForPopup(e,n,s,o){return new Promise(t=>{let i=new BroadcastChannel(R),c=!1,f=p=>{c||(c=!0,i.close(),window.clearInterval(g),o.dispatchEvent(new CustomEvent(u,{detail:p,bubbles:!0,composed:!0})),t(p))};i.onmessage=p=>{p.data?.ref===n&&f(p.data.result)};let g=window.setInterval(()=>{e.closed&&(window.clearInterval(g),window.setTimeout(()=>f({action:s,result:"CANCELLED"}),500))},400)})}url(e,n={}){let s=new URLSearchParams;for(let[t,i]of Object.entries(n))i!==void 0&&s.set(t,i);let o=s.toString();return`${this.basePath}${e}${o?`?${o}`:""}`}};function m(){return`${window.location.pathname}${window.location.search}${window.location.hash}`}var w={"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"},k=`
|
|
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,e="clearly-hub-button"){if(typeof customElements>"u"||customElements.get(e))return;class n extends HTMLElement{constructor(){super();a(this,"button");let t=this.attachShadow({mode:"open"});t.innerHTML=`<style>${k}</style><button part="button" type="button"></button>`,this.button=t.querySelector("button"),this.button.addEventListener("click",()=>this.run())}connectedCallback(){for(let t of["action","display","label","variant","size","data"])if(Object.prototype.hasOwnProperty.call(this,t)){let i=this[t];delete this[t],this[t]=i}this.render()}attributeChangedCallback(){this.render()}get action(){let t=this.getAttribute("action")??"sign-in";return t in w?t:"sign-in"}set action(t){this.setOrRemove("action",t)}get display(){return this.getAttribute("display")==="redirect"?"redirect":void 0}set display(t){this.setOrRemove("display",t)}get label(){return this.getAttribute("label")}set label(t){this.setOrRemove("label",t)}get variant(){return this.getAttribute("variant")}set variant(t){this.setOrRemove("variant",t)}get size(){return this.getAttribute("size")}set size(t){this.setOrRemove("size",t)}get data(){let t=this.getAttribute("data");return t?JSON.parse(t):void 0}set data(t){this.setOrRemove("data",typeof t=="string"||t==null?t:JSON.stringify(t))}setOrRemove(t,i){i==null?this.removeAttribute(t):this.setAttribute(t,i)}render(){let t=this.label??w[this.action];this.button.innerHTML=`${O}<span></span>`,this.button.querySelector("span").textContent=t}async run(){let t=this.action;if(t==="sign-in")return r.signIn();if(t==="sign-out")return r.signOut();if(S(t)){this.button.disabled=!0;try{await r.open(t,{display:this.display,data:this.data,eventTarget:this})}catch(i){console.error(i);let c={action:t,result:"FAILURE",error:i.message};this.dispatchEvent(new CustomEvent(u,{detail:c,bubbles:!0,composed:!0}))}finally{this.button.disabled=!1}}}}a(n,"observedAttributes",["action","label"]),customElements.define(e,n)}var d=new h;A(d);if(typeof window<"u"){let r=()=>{d.result().then(e=>{e&&d.deliverRedirectResult(e)})};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",r,{once:!0}):queueMicrotask(r)}var v=document.currentScript;d.configure({basePath:v?.dataset.basePath,display:v?.dataset.display==="redirect"?"redirect":void 0});window.clearlyHub=d;})();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { S as ScreenResult, a as ScreenDisplay, b as ScreenAction, c as ScreenData } from './screens-CZiYNLZI.js';
|
|
2
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';
|
|
3
3
|
|
|
4
|
+
declare const DEFAULT_BASE_PATH = "/clearly-hub";
|
|
5
|
+
declare const DEFAULT_HUB_URL = "https://hub.clearly.app";
|
|
6
|
+
declare const DEFAULT_AUTH_URL = "https://auth.clearly.app";
|
|
7
|
+
declare const DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
|
|
8
|
+
declare const ROUTES: {
|
|
9
|
+
readonly signIn: "/sign-in";
|
|
10
|
+
readonly callback: "/callback";
|
|
11
|
+
readonly signOut: "/sign-out";
|
|
12
|
+
readonly session: "/session";
|
|
13
|
+
readonly open: "/open";
|
|
14
|
+
readonly answer: "/answer";
|
|
15
|
+
readonly result: "/result";
|
|
16
|
+
readonly token: "/token";
|
|
17
|
+
};
|
|
18
|
+
declare const ANSWER_CHANNEL = "clearly-hub-answers";
|
|
19
|
+
interface AnswerMessage {
|
|
20
|
+
ref: string;
|
|
21
|
+
result: ScreenResult;
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
interface SessionUser {
|
|
5
25
|
sub: string;
|
|
6
26
|
email: string;
|
|
@@ -19,24 +39,9 @@ type Session = {
|
|
|
19
39
|
signedIn: true;
|
|
20
40
|
user: SessionUser;
|
|
21
41
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
declare const DEFAULT_AUTH_URL = "https://auth.clearly.app";
|
|
26
|
-
declare const DEFAULT_ISSUER = "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_1fM1Zd9mJ";
|
|
27
|
-
declare const ROUTES: {
|
|
28
|
-
readonly signIn: "/sign-in";
|
|
29
|
-
readonly callback: "/callback";
|
|
30
|
-
readonly signOut: "/sign-out";
|
|
31
|
-
readonly session: "/session";
|
|
32
|
-
readonly open: "/open";
|
|
33
|
-
readonly answer: "/answer";
|
|
34
|
-
readonly result: "/result";
|
|
35
|
-
};
|
|
36
|
-
declare const ANSWER_CHANNEL = "clearly-hub-answers";
|
|
37
|
-
interface AnswerMessage {
|
|
38
|
-
ref: string;
|
|
39
|
-
result: ScreenResult;
|
|
42
|
+
interface AccessToken {
|
|
43
|
+
accessToken: string;
|
|
44
|
+
expiresIn: number;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
interface ClientOptions {
|
|
@@ -54,14 +59,20 @@ declare class ClearlyHubClient {
|
|
|
54
59
|
private basePath;
|
|
55
60
|
private display;
|
|
56
61
|
private redirectResult?;
|
|
62
|
+
private unclaimedRedirectResult?;
|
|
63
|
+
private token?;
|
|
64
|
+
private tokenRequest?;
|
|
57
65
|
constructor(options?: ClientOptions);
|
|
58
66
|
configure(options: ClientOptions): this;
|
|
59
67
|
session(): Promise<Session>;
|
|
68
|
+
getToken(): Promise<string | null>;
|
|
60
69
|
signIn({ returnTo }?: {
|
|
61
70
|
returnTo?: string;
|
|
62
71
|
}): void;
|
|
63
72
|
signOut(): void;
|
|
64
73
|
open<A extends ScreenAction>(action: A, options?: OpenOptions<A>): Promise<ScreenResult<A>>;
|
|
74
|
+
onAnswer(callback: (result: ScreenResult) => void): () => void;
|
|
75
|
+
deliverRedirectResult(result: ScreenResult): void;
|
|
65
76
|
result(): Promise<ScreenResult | null>;
|
|
66
77
|
private waitForPopup;
|
|
67
78
|
private url;
|
|
@@ -72,4 +83,4 @@ declare function defineButton(client: ClearlyHubClient, tagName?: string): void;
|
|
|
72
83
|
declare const clearlyHub: ClearlyHubClient;
|
|
73
84
|
|
|
74
85
|
export { ANSWER_CHANNEL, ANSWER_EVENT, ClearlyHubClient, DEFAULT_AUTH_URL, DEFAULT_BASE_PATH, DEFAULT_HUB_URL, DEFAULT_ISSUER, ROUTES, ScreenAction, ScreenData, ScreenDisplay, ScreenResult, clearlyHub, defineButton };
|
|
75
|
-
export type { AnswerMessage, ClientOptions, HubSubscription, OpenOptions, Session, SessionUser };
|
|
86
|
+
export type { AccessToken, AnswerMessage, ClientOptions, HubSubscription, OpenOptions, Session, SessionUser };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,20 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
|
|
5
|
+
// ../shared/src/protocol.ts
|
|
6
|
+
var DEFAULT_BASE_PATH = "/clearly-hub";
|
|
7
|
+
var ROUTES = {
|
|
8
|
+
signIn: "/sign-in",
|
|
9
|
+
callback: "/callback",
|
|
10
|
+
signOut: "/sign-out",
|
|
11
|
+
session: "/session",
|
|
12
|
+
open: "/open",
|
|
13
|
+
answer: "/answer",
|
|
14
|
+
result: "/result",
|
|
15
|
+
token: "/token"
|
|
16
|
+
};
|
|
17
|
+
var ANSWER_CHANNEL = "clearly-hub-answers";
|
|
18
|
+
|
|
5
19
|
// ../shared/src/screens.ts
|
|
6
20
|
var SCREEN_ACTIONS = [
|
|
7
21
|
"SELECT_SUBSCRIPTION",
|
|
@@ -30,19 +44,6 @@ function checkScreenData(action, data) {
|
|
|
30
44
|
return void 0;
|
|
31
45
|
}
|
|
32
46
|
|
|
33
|
-
// ../shared/src/protocol.ts
|
|
34
|
-
var DEFAULT_BASE_PATH = "/clearly-hub";
|
|
35
|
-
var ROUTES = {
|
|
36
|
-
signIn: "/sign-in",
|
|
37
|
-
callback: "/callback",
|
|
38
|
-
signOut: "/sign-out",
|
|
39
|
-
session: "/session",
|
|
40
|
-
open: "/open",
|
|
41
|
-
answer: "/answer",
|
|
42
|
-
result: "/result"
|
|
43
|
-
};
|
|
44
|
-
var ANSWER_CHANNEL = "clearly-hub-answers";
|
|
45
|
-
|
|
46
47
|
// src/client.ts
|
|
47
48
|
var ANSWER_EVENT = "clearly-hub-answer";
|
|
48
49
|
var POPUP_WIDTH = 480;
|
|
@@ -51,6 +52,11 @@ var ClearlyHubClient = class {
|
|
|
51
52
|
__publicField(this, "basePath", DEFAULT_BASE_PATH);
|
|
52
53
|
__publicField(this, "display", "popup");
|
|
53
54
|
__publicField(this, "redirectResult");
|
|
55
|
+
// An answer after a redirect that no onAnswer() callback has had yet: the
|
|
56
|
+
// first callback to come gets it
|
|
57
|
+
__publicField(this, "unclaimedRedirectResult");
|
|
58
|
+
__publicField(this, "token");
|
|
59
|
+
__publicField(this, "tokenRequest");
|
|
54
60
|
this.configure(options);
|
|
55
61
|
}
|
|
56
62
|
configure(options) {
|
|
@@ -60,10 +66,37 @@ var ClearlyHubClient = class {
|
|
|
60
66
|
}
|
|
61
67
|
// Who's signed in. Which of your tenants they work for is your app's to know
|
|
62
68
|
async session() {
|
|
63
|
-
const response = await fetch(this.url(ROUTES.session), {
|
|
64
|
-
|
|
69
|
+
const response = await fetch(this.url(ROUTES.session), {
|
|
70
|
+
credentials: "same-origin"
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok)
|
|
73
|
+
throw new Error(`Clearly.HUB session failed (${response.status})`);
|
|
65
74
|
return response.json();
|
|
66
75
|
}
|
|
76
|
+
// The person's Clearly.HUB key, for protected and paid data (a resource's
|
|
77
|
+
// url from DATA_PICKER): send it as the Authorization header, for example
|
|
78
|
+
// from your map layers. Null when nobody's signed in. Kept until a minute
|
|
79
|
+
// before it expires, so ask again for each use instead of storing it.
|
|
80
|
+
async getToken() {
|
|
81
|
+
if (this.token && this.token.expiresAt > Date.now() + 6e4)
|
|
82
|
+
return this.token.value;
|
|
83
|
+
this.tokenRequest ?? (this.tokenRequest = fetch(this.url(ROUTES.token), {
|
|
84
|
+
credentials: "same-origin"
|
|
85
|
+
}).then(async (response) => {
|
|
86
|
+
if (response.status === 401) return null;
|
|
87
|
+
if (!response.ok)
|
|
88
|
+
throw new Error(`Clearly.HUB token failed (${response.status})`);
|
|
89
|
+
const { accessToken, expiresIn } = await response.json();
|
|
90
|
+
return {
|
|
91
|
+
value: accessToken,
|
|
92
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
93
|
+
};
|
|
94
|
+
}).finally(() => {
|
|
95
|
+
this.tokenRequest = void 0;
|
|
96
|
+
}));
|
|
97
|
+
this.token = await this.tokenRequest ?? void 0;
|
|
98
|
+
return this.token?.value ?? null;
|
|
99
|
+
}
|
|
67
100
|
signIn({ returnTo = currentPath() } = {}) {
|
|
68
101
|
window.location.assign(this.url(ROUTES.signIn, { returnTo }));
|
|
69
102
|
}
|
|
@@ -75,7 +108,8 @@ var ClearlyHubClient = class {
|
|
|
75
108
|
// back to a redirect.
|
|
76
109
|
open(action, options = {}) {
|
|
77
110
|
const dataProblem = checkScreenData(action, options.data);
|
|
78
|
-
if (dataProblem)
|
|
111
|
+
if (dataProblem)
|
|
112
|
+
return Promise.reject(new Error(`Clearly.HUB: ${dataProblem}`));
|
|
79
113
|
const ref = crypto.randomUUID();
|
|
80
114
|
const display = options.display ?? this.display;
|
|
81
115
|
const query = {
|
|
@@ -90,16 +124,50 @@ var ClearlyHubClient = class {
|
|
|
90
124
|
"clearly-hub-screen",
|
|
91
125
|
`width=${POPUP_WIDTH},height=${screen.availHeight},left=${screen.availWidth - POPUP_WIDTH}`
|
|
92
126
|
);
|
|
93
|
-
if (popup)
|
|
127
|
+
if (popup)
|
|
128
|
+
return this.waitForPopup(
|
|
129
|
+
popup,
|
|
130
|
+
ref,
|
|
131
|
+
action,
|
|
132
|
+
options.eventTarget ?? window
|
|
133
|
+
);
|
|
94
134
|
}
|
|
95
|
-
window.location.assign(
|
|
135
|
+
window.location.assign(
|
|
136
|
+
this.url(ROUTES.open, { ...query, display: "redirect" })
|
|
137
|
+
);
|
|
96
138
|
return new Promise(() => {
|
|
97
139
|
});
|
|
98
140
|
}
|
|
141
|
+
// Every answer, once each: from a popup, and after a redirect, also when
|
|
142
|
+
// that one arrived before you called this (as in React, where listeners
|
|
143
|
+
// come after the first render). Returns a function that stops listening.
|
|
144
|
+
onAnswer(callback) {
|
|
145
|
+
const listener = (event) => {
|
|
146
|
+
const result = event.detail;
|
|
147
|
+
if (result === this.unclaimedRedirectResult)
|
|
148
|
+
this.unclaimedRedirectResult = void 0;
|
|
149
|
+
callback(result);
|
|
150
|
+
};
|
|
151
|
+
window.addEventListener(ANSWER_EVENT, listener);
|
|
152
|
+
const unclaimed = this.unclaimedRedirectResult;
|
|
153
|
+
if (unclaimed) {
|
|
154
|
+
this.unclaimedRedirectResult = void 0;
|
|
155
|
+
callback(unclaimed);
|
|
156
|
+
}
|
|
157
|
+
return () => window.removeEventListener(ANSWER_EVENT, listener);
|
|
158
|
+
}
|
|
159
|
+
// On page load: announces the answer after a redirect as the
|
|
160
|
+
// clearly-hub-answer event, and keeps it for an onAnswer() that comes later
|
|
161
|
+
deliverRedirectResult(result) {
|
|
162
|
+
this.unclaimedRedirectResult = result;
|
|
163
|
+
window.dispatchEvent(new CustomEvent(ANSWER_EVENT, { detail: result }));
|
|
164
|
+
}
|
|
99
165
|
// After a redirect screen: its answer, once, on the page it came back to.
|
|
100
166
|
// Null when there is none.
|
|
101
167
|
result() {
|
|
102
|
-
this.redirectResult ?? (this.redirectResult = fetch(this.url(ROUTES.result), {
|
|
168
|
+
this.redirectResult ?? (this.redirectResult = fetch(this.url(ROUTES.result), {
|
|
169
|
+
credentials: "same-origin"
|
|
170
|
+
}).then((response) => response.ok ? response.json() : null).then((message) => message?.result ?? null).catch(() => null));
|
|
103
171
|
return this.redirectResult;
|
|
104
172
|
}
|
|
105
173
|
waitForPopup(popup, ref, action, eventTarget) {
|
|
@@ -112,12 +180,17 @@ var ClearlyHubClient = class {
|
|
|
112
180
|
channel.close();
|
|
113
181
|
window.clearInterval(closedCheck);
|
|
114
182
|
eventTarget.dispatchEvent(
|
|
115
|
-
new CustomEvent(ANSWER_EVENT, {
|
|
183
|
+
new CustomEvent(ANSWER_EVENT, {
|
|
184
|
+
detail: result,
|
|
185
|
+
bubbles: true,
|
|
186
|
+
composed: true
|
|
187
|
+
})
|
|
116
188
|
);
|
|
117
189
|
resolve(result);
|
|
118
190
|
};
|
|
119
191
|
channel.onmessage = (event) => {
|
|
120
|
-
if (event.data?.ref === ref)
|
|
192
|
+
if (event.data?.ref === ref)
|
|
193
|
+
finish(event.data.result);
|
|
121
194
|
};
|
|
122
195
|
const closedCheck = window.setInterval(() => {
|
|
123
196
|
if (popup.closed) {
|
|
@@ -173,7 +246,8 @@ var STYLE = `
|
|
|
173
246
|
`;
|
|
174
247
|
var ICON = `<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>`;
|
|
175
248
|
function defineButton(client, tagName = "clearly-hub-button") {
|
|
176
|
-
if (typeof customElements === "undefined" || customElements.get(tagName))
|
|
249
|
+
if (typeof customElements === "undefined" || customElements.get(tagName))
|
|
250
|
+
return;
|
|
177
251
|
class ClearlyHubButton extends HTMLElement {
|
|
178
252
|
constructor() {
|
|
179
253
|
super();
|
|
@@ -184,7 +258,14 @@ function defineButton(client, tagName = "clearly-hub-button") {
|
|
|
184
258
|
this.button.addEventListener("click", () => this.run());
|
|
185
259
|
}
|
|
186
260
|
connectedCallback() {
|
|
187
|
-
for (const name of [
|
|
261
|
+
for (const name of [
|
|
262
|
+
"action",
|
|
263
|
+
"display",
|
|
264
|
+
"label",
|
|
265
|
+
"variant",
|
|
266
|
+
"size",
|
|
267
|
+
"data"
|
|
268
|
+
]) {
|
|
188
269
|
if (Object.prototype.hasOwnProperty.call(this, name)) {
|
|
189
270
|
const value = this[name];
|
|
190
271
|
delete this[name];
|
|
@@ -264,9 +345,17 @@ function defineButton(client, tagName = "clearly-hub-button") {
|
|
|
264
345
|
});
|
|
265
346
|
} catch (error) {
|
|
266
347
|
console.error(error);
|
|
267
|
-
const result = {
|
|
348
|
+
const result = {
|
|
349
|
+
action,
|
|
350
|
+
result: "FAILURE",
|
|
351
|
+
error: error.message
|
|
352
|
+
};
|
|
268
353
|
this.dispatchEvent(
|
|
269
|
-
new CustomEvent(ANSWER_EVENT, {
|
|
354
|
+
new CustomEvent(ANSWER_EVENT, {
|
|
355
|
+
detail: result,
|
|
356
|
+
bubbles: true,
|
|
357
|
+
composed: true
|
|
358
|
+
})
|
|
270
359
|
);
|
|
271
360
|
} finally {
|
|
272
361
|
this.button.disabled = false;
|
|
@@ -281,11 +370,16 @@ function defineButton(client, tagName = "clearly-hub-button") {
|
|
|
281
370
|
var clearlyHub = new ClearlyHubClient();
|
|
282
371
|
defineButton(clearlyHub);
|
|
283
372
|
if (typeof window !== "undefined") {
|
|
284
|
-
|
|
373
|
+
const deliver = () => {
|
|
285
374
|
void clearlyHub.result().then((result) => {
|
|
286
|
-
if (result)
|
|
375
|
+
if (result) clearlyHub.deliverRedirectResult(result);
|
|
287
376
|
});
|
|
288
|
-
}
|
|
377
|
+
};
|
|
378
|
+
if (document.readyState === "loading") {
|
|
379
|
+
document.addEventListener("DOMContentLoaded", deliver, { once: true });
|
|
380
|
+
} else {
|
|
381
|
+
queueMicrotask(deliver);
|
|
382
|
+
}
|
|
289
383
|
}
|
|
290
384
|
export {
|
|
291
385
|
ANSWER_EVENT,
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
2
1
|
import { ScreenAction, ScreenDisplay } from './index.js';
|
|
2
|
+
import { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
3
3
|
|
|
4
4
|
interface ClearlyHubButtonAttributes {
|
|
5
5
|
action?: ScreenAction | 'sign-in' | 'sign-out';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clearly-hub/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|