@welshare/react 0.0.1-alpha.1

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.
Files changed (42) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +124 -0
  3. package/dist/esm/components/connect-button.d.ts +6 -0
  4. package/dist/esm/components/connect-button.d.ts.map +1 -0
  5. package/dist/esm/components/connect-button.js +58 -0
  6. package/dist/esm/hooks/use-welshare.d.ts +10 -0
  7. package/dist/esm/hooks/use-welshare.d.ts.map +1 -0
  8. package/dist/esm/hooks/use-welshare.js +111 -0
  9. package/dist/esm/index.d.ts +7 -0
  10. package/dist/esm/index.d.ts.map +1 -0
  11. package/dist/esm/index.js +10 -0
  12. package/dist/esm/package.json +3 -0
  13. package/dist/esm/types.d.ts +32 -0
  14. package/dist/esm/types.d.ts.map +1 -0
  15. package/dist/esm/types.js +1 -0
  16. package/dist/node_modules/@welshare/react/.gitignore +6 -0
  17. package/dist/node_modules/@welshare/react/.npmignore +34 -0
  18. package/dist/node_modules/@welshare/react/.tshy/build.json +8 -0
  19. package/dist/node_modules/@welshare/react/.tshy/esm.json +16 -0
  20. package/dist/node_modules/@welshare/react/LICENSE +7 -0
  21. package/dist/node_modules/@welshare/react/README.md +124 -0
  22. package/dist/node_modules/@welshare/react/dist/esm/components/connect-button.d.ts +6 -0
  23. package/dist/node_modules/@welshare/react/dist/esm/components/connect-button.d.ts.map +1 -0
  24. package/dist/node_modules/@welshare/react/dist/esm/components/connect-button.js +58 -0
  25. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.d.ts +10 -0
  26. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.d.ts.map +1 -0
  27. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.js +111 -0
  28. package/dist/node_modules/@welshare/react/dist/esm/index.d.ts +7 -0
  29. package/dist/node_modules/@welshare/react/dist/esm/index.d.ts.map +1 -0
  30. package/dist/node_modules/@welshare/react/dist/esm/index.js +10 -0
  31. package/dist/node_modules/@welshare/react/dist/esm/package.json +3 -0
  32. package/dist/node_modules/@welshare/react/dist/esm/types.d.ts +32 -0
  33. package/dist/node_modules/@welshare/react/dist/esm/types.d.ts.map +1 -0
  34. package/dist/node_modules/@welshare/react/dist/esm/types.js +1 -0
  35. package/dist/node_modules/@welshare/react/eslint.config.mjs +4 -0
  36. package/dist/node_modules/@welshare/react/package.json +88 -0
  37. package/dist/node_modules/@welshare/react/src/components/connect-button.tsx +99 -0
  38. package/dist/node_modules/@welshare/react/src/hooks/use-welshare.ts +148 -0
  39. package/dist/node_modules/@welshare/react/src/index.ts +13 -0
  40. package/dist/node_modules/@welshare/react/src/types.ts +37 -0
  41. package/dist/node_modules/@welshare/react/tsconfig.json +21 -0
  42. package/package.json +87 -0
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export const ConnectWelshareButton = (props) => {
3
+ // Self-contained styles that won't conflict with external applications
4
+ const buttonStyles = {
5
+ // Reset any inherited styles
6
+ all: 'unset',
7
+ boxSizing: 'border-box',
8
+ // Layout
9
+ display: 'inline-flex',
10
+ alignItems: 'center',
11
+ justifyContent: 'center',
12
+ width: '100%',
13
+ minHeight: '44px',
14
+ padding: '12px 24px',
15
+ // Typography
16
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
17
+ fontSize: '16px',
18
+ fontWeight: '600',
19
+ lineHeight: '1.5',
20
+ textAlign: 'center',
21
+ textDecoration: 'none',
22
+ whiteSpace: 'nowrap',
23
+ // Visual styling
24
+ backgroundColor: '#0198ff',
25
+ background: 'linear-gradient(135deg, #0198ff 0%, #16ffef 100%)',
26
+ color: '#ffffff',
27
+ border: 'none',
28
+ borderRadius: '8px',
29
+ cursor: 'pointer',
30
+ transition: 'all 0.2s ease-in-out',
31
+ // Accessibility
32
+ userSelect: 'none',
33
+ WebkitUserSelect: 'none',
34
+ MozUserSelect: 'none',
35
+ // Focus states
36
+ outline: 'none',
37
+ };
38
+ // Handle hover and focus states since inline styles don't support pseudo-selectors
39
+ const handleMouseEnter = (e) => {
40
+ e.currentTarget.style.transform = 'translateY(-1px)';
41
+ e.currentTarget.style.boxShadow = '0 4px 12px rgba(1, 152, 255, 0.3)';
42
+ };
43
+ const handleMouseLeave = (e) => {
44
+ e.currentTarget.style.transform = 'translateY(0)';
45
+ e.currentTarget.style.boxShadow = '0 2px 6px rgba(1, 152, 255, 0.2)';
46
+ };
47
+ const handleMouseDown = (e) => {
48
+ e.currentTarget.style.transform = 'translateY(0)';
49
+ e.currentTarget.style.boxShadow = '0 2px 6px rgba(1, 152, 255, 0.2)';
50
+ };
51
+ const handleFocus = (e) => {
52
+ e.currentTarget.style.boxShadow = '0 0 0 3px rgba(1, 152, 255, 0.3)';
53
+ };
54
+ const handleBlur = (e) => {
55
+ e.currentTarget.style.boxShadow = '0 2px 6px rgba(1, 152, 255, 0.2)';
56
+ };
57
+ return (_jsx("button", { onClick: props.openWallet, style: buttonStyles, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onMouseDown: handleMouseDown, onFocus: handleFocus, onBlur: handleBlur, type: "button", children: props.children || "Connect Welshare Profile" }));
58
+ };
@@ -0,0 +1,10 @@
1
+ import { SubmissionPayload, SubmissionSchemaId, WelshareConnectionOptions } from "@/types.js";
2
+ export declare const useWelshare: (props: WelshareConnectionOptions) => {
3
+ isConnected: boolean;
4
+ storageKey: string | undefined;
5
+ openWallet: () => void;
6
+ isDialogOpen: boolean;
7
+ submitData: <T>(schemaId: SubmissionSchemaId, submission: SubmissionPayload<T>) => void;
8
+ isSubmitting: boolean;
9
+ };
10
+ //# sourceMappingURL=use-welshare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-welshare.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-welshare.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,WAAW,UAAW,yBAAyB;;;;;iBAkFtC,CAAC,YACT,kBAAkB,cAChB,iBAAiB,CAAC,CAAC,CAAC;;CAuDnC,CAAC"}
@@ -0,0 +1,111 @@
1
+ import { useEffect, useState } from "react";
2
+ export const useWelshare = (props) => {
3
+ const [storageKey, setStorageKey] = useState();
4
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
5
+ const [dialogWindow, setDialogWindow] = useState(null);
6
+ const [messageIdCounter, setMessageIdCounter] = useState(0);
7
+ const [isSubmitting, setIsSubmitting] = useState(false);
8
+ const options = {
9
+ environment: "development",
10
+ apiBaseUrl: "https://wallet.welshare.health",
11
+ ...props,
12
+ };
13
+ const WELSHARE_WALLET_URL = `${options.apiBaseUrl}/wallet-external`;
14
+ useEffect(() => {
15
+ const handleMessage = (event) => {
16
+ // Verify origin for security
17
+ if (event.origin !== new URL(WELSHARE_WALLET_URL).origin) {
18
+ return;
19
+ }
20
+ const message = event.data;
21
+ let errorMessage = "";
22
+ switch (message.type) {
23
+ case "ERROR":
24
+ errorMessage = message.payload.error || "An unknown error occurred";
25
+ console.error("Welshare Wallet sent an error:", errorMessage);
26
+ setIsSubmitting(false);
27
+ options.callbacks.onError?.(errorMessage);
28
+ break;
29
+ case "DIALOG_READY":
30
+ setIsDialogOpen(true);
31
+ console.debug("Welshare Wallet Dialog is ready");
32
+ break;
33
+ case "SESSION_READY":
34
+ if (!message.payload.sessionPublicKey) {
35
+ errorMessage = "No session public key found";
36
+ console.error("Welshare Wallet sent a session without a key:", message.payload);
37
+ setStorageKey(undefined);
38
+ options.callbacks.onError?.(errorMessage);
39
+ break;
40
+ }
41
+ console.debug("Welshare Profile Session ready", message.payload);
42
+ setStorageKey(message.payload.sessionPublicKey);
43
+ options.callbacks.onSessionReady?.(message.payload.sessionPublicKey);
44
+ break;
45
+ case "DIALOG_CLOSING":
46
+ console.debug("Welshare Wallet Dialog is closing");
47
+ setStorageKey(undefined);
48
+ setIsDialogOpen(false);
49
+ options.callbacks.onDialogClosing?.();
50
+ break;
51
+ case "DATA_UPLOADED":
52
+ console.debug("data uploaded", message.payload);
53
+ setIsSubmitting(false);
54
+ options.callbacks.onUploaded?.(message.payload);
55
+ break;
56
+ default:
57
+ console.log("Received unexpected message from Welshare Wallet:", message);
58
+ }
59
+ };
60
+ window.addEventListener("message", handleMessage);
61
+ return () => {
62
+ window.removeEventListener("message", handleMessage);
63
+ };
64
+ }, [WELSHARE_WALLET_URL, options.callbacks]);
65
+ /**
66
+ * @param schemaType a welshare schema type uid
67
+ * @param submission your submission that validates against the schema type
68
+ */
69
+ const submitData = (schemaId, submission) => {
70
+ if (!dialogWindow) {
71
+ throw new Error("Dialog window not open");
72
+ }
73
+ const submissionPayload = {
74
+ applicationId: options.applicationId,
75
+ schemaId,
76
+ timestamp: new Date(),
77
+ submission,
78
+ };
79
+ //todo: before submitting data, consider verifying it locally here;
80
+ // 1. request the actual schema using a welshare api
81
+ // 2. create a json schema validator
82
+ // 3. run payload against that validator
83
+ // 4. submit
84
+ const message = {
85
+ type: "SUBMIT_DATA",
86
+ payload: submissionPayload,
87
+ id: String(messageIdCounter),
88
+ };
89
+ dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
90
+ setIsSubmitting(true);
91
+ setMessageIdCounter((prev) => prev + 1);
92
+ };
93
+ const openWallet = () => {
94
+ const width = 800;
95
+ const height = 600;
96
+ const left = window.screenX + (window.outerWidth - width) / 2;
97
+ const top = window.screenY + (window.outerHeight - height) / 2;
98
+ const newWindow = window.open(WELSHARE_WALLET_URL, "Welshare Wallet", `width=${width},height=${height},left=${left},top=${top}`);
99
+ if (newWindow) {
100
+ setDialogWindow(newWindow);
101
+ }
102
+ };
103
+ return {
104
+ isConnected: !!storageKey,
105
+ storageKey,
106
+ openWallet,
107
+ isDialogOpen,
108
+ submitData,
109
+ isSubmitting,
110
+ };
111
+ };
@@ -0,0 +1,7 @@
1
+ export { ConnectWelshareButton } from "./components/connect-button.js";
2
+ export { useWelshare } from "./hooks/use-welshare.js";
3
+ export declare const Schemas: {
4
+ QuestionnaireResponse: string;
5
+ ReflexSubmission: string;
6
+ };
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAEvE,OAAO,EACL,WAAW,EACZ,MAAM,yBAAyB,CAAC;AAGjC,eAAO,MAAM,OAAO;;;CAGnB,CAAC"}
@@ -0,0 +1,10 @@
1
+ //todo: this is not tree shaken and leaks nillion dependencies transitively. Consider pulling it up
2
+ //import { QuestionnaireResponseSchema, ReflexSubmissionSchema } from "@welshare/sdk";
3
+ export { ConnectWelshareButton } from "./components/connect-button.js";
4
+ // ---- hooks ----
5
+ export { useWelshare } from "./hooks/use-welshare.js";
6
+ //todo: import them from the SDK or a dedicated SDK constants export
7
+ export const Schemas = {
8
+ QuestionnaireResponse: "b14b538f-7de3-4767-ad77-464d755d78bd", //QuestionnaireResponseSchema.schemaUid,
9
+ ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c" //ReflexSubmissionSchema.schemaUid,
10
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,32 @@
1
+ export interface DialogMessage {
2
+ type: string;
3
+ payload?: any;
4
+ id?: string;
5
+ }
6
+ export type WelshareEnvironment = "development" | "production";
7
+ /**
8
+ * a welshare schema type uid
9
+ */
10
+ export type SubmissionSchemaId = string;
11
+ export interface SubmissionPayload<T> {
12
+ applicationId: string;
13
+ timestamp: Date;
14
+ schemaId: SubmissionSchemaId;
15
+ submission: T;
16
+ }
17
+ export interface DataSubmissionDialogMessage extends DialogMessage {
18
+ payload: SubmissionPayload<unknown>;
19
+ }
20
+ export interface WelshareConnectionOptions {
21
+ applicationId: string;
22
+ apiBaseUrl?: string;
23
+ environment?: WelshareEnvironment;
24
+ callbacks: {
25
+ onUploaded?: (payload: SubmissionPayload<unknown>) => void;
26
+ onError?: (error: string) => void;
27
+ onSessionReady?: (sessionPubKey: string) => void;
28
+ onDialogClosing?: () => void;
29
+ onDialogReady?: () => void;
30
+ };
31
+ }
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,YAAY,CAAC;AAE/D;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAA;AAEvC,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,EAAE,CAAC,CAAC;CACf;AAED,MAAM,WAAW,2BAA4B,SAAQ,aAAa;IAChE,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,MAAM,CAAC;IAEtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,SAAS,EAAE;QACT,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;QAC3D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;QAClC,cAAc,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;QACjD,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;QAC7B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;KAC5B,CAAC;CACH"}
@@ -0,0 +1,4 @@
1
+ import { config } from "@workspace/eslint-config/react-internal"
2
+
3
+ /** @type {import("eslint").Linter.Config} */
4
+ export default config
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@welshare/react",
3
+ "version": "0.0.1-alpha.1",
4
+ "description": "React library for integrating with Welshare's sovereign data sharing platform",
5
+ "keywords": [
6
+ "react",
7
+ "welshare",
8
+ "data-sharing",
9
+ "health",
10
+ "questionnaire",
11
+ "fhir"
12
+ ],
13
+ "author": "Welshare UG (haftungsbeschränkt)",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/welshare/surveys-monorepo.git",
18
+ "directory": "packages/welshare-react"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/welshare/surveys-monorepo/issues"
22
+ },
23
+ "homepage": "https://welshare.health",
24
+ "type": "module",
25
+ "scripts": {
26
+ "lint": "eslint . --max-warnings 10",
27
+ "build": "tshy",
28
+ "build:clean": "rm -rf ./dist",
29
+ "test": "vitest",
30
+ "test:run": "vitest run",
31
+ "test:coverage": "vitest run --coverage",
32
+ "prepublishOnly": "npm run build:clean && npm run build"
33
+ },
34
+ "peerDependencies": {
35
+ "react": "^19",
36
+ "react-dom": "^19"
37
+ },
38
+ "devDependencies": {
39
+ "@turbo/gen": "^2.4.2",
40
+ "@types/node": "^22",
41
+ "@types/react": "^19",
42
+ "@workspace/eslint-config": "workspace:*",
43
+ "@workspace/typescript-config": "workspace:*",
44
+ "tshy": "^3.0.2",
45
+ "typescript": "^5.7.3",
46
+ "vitest": "^3.2.4",
47
+ "@vitest/ui": "^3.2.4",
48
+ "@vitest/coverage-v8": "^3.2.4",
49
+ "jsdom": "^26.1.0"
50
+ },
51
+ "tshy": {
52
+ "dialects": [
53
+ "esm"
54
+ ],
55
+ "exclude": [
56
+ "src/**/__tests__/**"
57
+ ],
58
+ "exports": {
59
+ "./package.json": "./package.json",
60
+ "./types": "./src/types.ts",
61
+ ".": "./src/index.ts"
62
+ }
63
+ },
64
+ "exports": {
65
+ "./package.json": "./package.json",
66
+ "./types": {
67
+ "import": {
68
+ "types": "./dist/esm/types.d.ts",
69
+ "default": "./dist/esm/types.js"
70
+ }
71
+ },
72
+ ".": {
73
+ "import": {
74
+ "types": "./dist/esm/index.d.ts",
75
+ "default": "./dist/esm/index.js"
76
+ }
77
+ }
78
+ },
79
+ "module": "./dist/esm/index.js",
80
+ "files": [
81
+ "dist/**/*",
82
+ "README.md",
83
+ "LICENSE"
84
+ ],
85
+ "engines": {
86
+ "node": ">=20.0.0"
87
+ }
88
+ }
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+
3
+ export const ConnectWelshareButton = (props: {
4
+ openWallet: () => void;
5
+ children?: React.ReactNode;
6
+ }) => {
7
+ // Self-contained styles that won't conflict with external applications
8
+ const buttonStyles: React.CSSProperties = {
9
+ // Reset any inherited styles
10
+ all: 'unset',
11
+ boxSizing: 'border-box',
12
+
13
+ // Layout
14
+ display: 'inline-flex',
15
+ alignItems: 'center',
16
+ justifyContent: 'center',
17
+ width: '100%',
18
+ minHeight: '44px',
19
+ padding: '12px 24px',
20
+
21
+ // Typography
22
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
23
+ fontSize: '16px',
24
+ fontWeight: '600',
25
+ lineHeight: '1.5',
26
+ textAlign: 'center',
27
+ textDecoration: 'none',
28
+ whiteSpace: 'nowrap',
29
+
30
+ // Visual styling
31
+ backgroundColor: '#0198ff',
32
+ background: 'linear-gradient(135deg, #0198ff 0%, #16ffef 100%)',
33
+ color: '#ffffff',
34
+ border: 'none',
35
+ borderRadius: '8px',
36
+ cursor: 'pointer',
37
+ transition: 'all 0.2s ease-in-out',
38
+
39
+ // Accessibility
40
+ userSelect: 'none',
41
+ WebkitUserSelect: 'none',
42
+ MozUserSelect: 'none',
43
+
44
+ // Focus states
45
+ outline: 'none',
46
+ };
47
+
48
+ // Handle hover and focus states since inline styles don't support pseudo-selectors
49
+ const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
50
+ e.currentTarget.style.transform = 'translateY(-1px)';
51
+ e.currentTarget.style.boxShadow = '0 4px 12px rgba(1, 152, 255, 0.3)';
52
+ };
53
+
54
+ const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
55
+ e.currentTarget.style.transform = 'translateY(0)';
56
+ e.currentTarget.style.boxShadow = '0 2px 6px rgba(1, 152, 255, 0.2)';
57
+ };
58
+
59
+ const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
60
+ e.currentTarget.style.transform = 'translateY(0)';
61
+ e.currentTarget.style.boxShadow = '0 2px 6px rgba(1, 152, 255, 0.2)';
62
+ };
63
+
64
+ const handleFocus = (e: React.FocusEvent<HTMLButtonElement>) => {
65
+ e.currentTarget.style.boxShadow = '0 0 0 3px rgba(1, 152, 255, 0.3)';
66
+ };
67
+
68
+ const handleBlur = (e: React.FocusEvent<HTMLButtonElement>) => {
69
+ e.currentTarget.style.boxShadow = '0 2px 6px rgba(1, 152, 255, 0.2)';
70
+ };
71
+
72
+ return (
73
+ <button
74
+ onClick={props.openWallet}
75
+ style={buttonStyles}
76
+ onMouseEnter={handleMouseEnter}
77
+ onMouseLeave={handleMouseLeave}
78
+ onMouseDown={handleMouseDown}
79
+ onFocus={handleFocus}
80
+ onBlur={handleBlur}
81
+ type="button"
82
+ >
83
+ {props.children || "Connect Welshare Profile"}
84
+ </button>
85
+ );
86
+ };
87
+
88
+ /* this one was used in the old reflex app
89
+ *
90
+ * <div className="p-[3px] bg-gradient-to-br from-[#0198ff]/80 to-[#16ffef]/80 rounded-lg">
91
+ <button
92
+ onClick={openDialog}
93
+ className="bg-black text-white px-6 py-3 rounded-lg w-full"
94
+ >
95
+ Connect to Welshare
96
+ </button>
97
+ </div>
98
+ *
99
+ */
@@ -0,0 +1,148 @@
1
+ import {
2
+ DialogMessage,
3
+ SubmissionPayload,
4
+ SubmissionSchemaId,
5
+ WelshareConnectionOptions,
6
+ } from "@/types.js";
7
+ import { useEffect, useState } from "react";
8
+
9
+ export const useWelshare = (props: WelshareConnectionOptions) => {
10
+ const [storageKey, setStorageKey] = useState<string>();
11
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
12
+ const [dialogWindow, setDialogWindow] = useState<Window | null>(null);
13
+ const [messageIdCounter, setMessageIdCounter] = useState(0);
14
+ const [isSubmitting, setIsSubmitting] = useState(false);
15
+
16
+ const options: WelshareConnectionOptions = {
17
+ environment: "development",
18
+ apiBaseUrl: "https://wallet.welshare.health",
19
+ ...props,
20
+ };
21
+
22
+ const WELSHARE_WALLET_URL = `${options.apiBaseUrl}/wallet-external`;
23
+
24
+ useEffect(() => {
25
+ const handleMessage = (event: MessageEvent<DialogMessage>) => {
26
+ // Verify origin for security
27
+ if (event.origin !== new URL(WELSHARE_WALLET_URL).origin) {
28
+ return;
29
+ }
30
+
31
+ const message = event.data;
32
+ let errorMessage = "";
33
+ switch (message.type) {
34
+ case "ERROR":
35
+ errorMessage = message.payload.error || "An unknown error occurred";
36
+ console.error("Welshare Wallet sent an error:", errorMessage);
37
+ setIsSubmitting(false);
38
+ options.callbacks.onError?.(errorMessage);
39
+ break;
40
+ case "DIALOG_READY":
41
+ setIsDialogOpen(true);
42
+ console.debug("Welshare Wallet Dialog is ready");
43
+ break;
44
+ case "SESSION_READY":
45
+ if (!message.payload.sessionPublicKey) {
46
+ errorMessage = "No session public key found";
47
+ console.error(
48
+ "Welshare Wallet sent a session without a key:",
49
+ message.payload
50
+ );
51
+ setStorageKey(undefined);
52
+ options.callbacks.onError?.(errorMessage);
53
+ break;
54
+ }
55
+ console.debug("Welshare Profile Session ready", message.payload);
56
+ setStorageKey(message.payload.sessionPublicKey);
57
+ options.callbacks.onSessionReady?.(message.payload.sessionPublicKey);
58
+ break;
59
+
60
+ case "DIALOG_CLOSING":
61
+ console.debug("Welshare Wallet Dialog is closing");
62
+ setStorageKey(undefined);
63
+ setIsDialogOpen(false);
64
+ options.callbacks.onDialogClosing?.();
65
+ break;
66
+
67
+ case "DATA_UPLOADED":
68
+ console.debug("data uploaded", message.payload);
69
+ setIsSubmitting(false);
70
+ options.callbacks.onUploaded?.(message.payload);
71
+ break;
72
+ default:
73
+ console.log(
74
+ "Received unexpected message from Welshare Wallet:",
75
+ message
76
+ );
77
+ }
78
+ };
79
+
80
+ window.addEventListener("message", handleMessage);
81
+
82
+ return () => {
83
+ window.removeEventListener("message", handleMessage);
84
+ };
85
+ }, [WELSHARE_WALLET_URL, options.callbacks]);
86
+
87
+ /**
88
+ * @param schemaType a welshare schema type uid
89
+ * @param submission your submission that validates against the schema type
90
+ */
91
+ const submitData = <T>(
92
+ schemaId: SubmissionSchemaId,
93
+ submission: SubmissionPayload<T>
94
+ ) => {
95
+ if (!dialogWindow) {
96
+ throw new Error("Dialog window not open");
97
+ }
98
+
99
+ const submissionPayload = {
100
+ applicationId: options.applicationId,
101
+ schemaId,
102
+ timestamp: new Date(),
103
+ submission,
104
+ };
105
+
106
+ //todo: before submitting data, consider verifying it locally here;
107
+ // 1. request the actual schema using a welshare api
108
+ // 2. create a json schema validator
109
+ // 3. run payload against that validator
110
+ // 4. submit
111
+
112
+ const message: DialogMessage = {
113
+ type: "SUBMIT_DATA",
114
+ payload: submissionPayload,
115
+ id: String(messageIdCounter),
116
+ };
117
+
118
+ dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
119
+ setIsSubmitting(true);
120
+ setMessageIdCounter((prev) => prev + 1);
121
+ };
122
+
123
+ const openWallet = () => {
124
+ const width = 800;
125
+ const height = 600;
126
+ const left = window.screenX + (window.outerWidth - width) / 2;
127
+ const top = window.screenY + (window.outerHeight - height) / 2;
128
+
129
+ const newWindow = window.open(
130
+ WELSHARE_WALLET_URL,
131
+ "Welshare Wallet",
132
+ `width=${width},height=${height},left=${left},top=${top}`
133
+ );
134
+
135
+ if (newWindow) {
136
+ setDialogWindow(newWindow);
137
+ }
138
+ };
139
+
140
+ return {
141
+ isConnected: !!storageKey,
142
+ storageKey,
143
+ openWallet,
144
+ isDialogOpen,
145
+ submitData,
146
+ isSubmitting,
147
+ };
148
+ };
@@ -0,0 +1,13 @@
1
+ //todo: this is not tree shaken and leaks nillion dependencies transitively. Consider pulling it up
2
+ //import { QuestionnaireResponseSchema, ReflexSubmissionSchema } from "@welshare/sdk";
3
+ export { ConnectWelshareButton } from "./components/connect-button.js";
4
+ // ---- hooks ----
5
+ export {
6
+ useWelshare
7
+ } from "./hooks/use-welshare.js";
8
+
9
+ //todo: import them from the SDK or a dedicated SDK constants export
10
+ export const Schemas = {
11
+ QuestionnaireResponse: "b14b538f-7de3-4767-ad77-464d755d78bd", //QuestionnaireResponseSchema.schemaUid,
12
+ ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c" //ReflexSubmissionSchema.schemaUid,
13
+ };
@@ -0,0 +1,37 @@
1
+ export interface DialogMessage {
2
+ type: string;
3
+ payload?: any;
4
+ id?: string;
5
+ }
6
+
7
+ export type WelshareEnvironment = "development" | "production";
8
+
9
+ /**
10
+ * a welshare schema type uid
11
+ */
12
+ export type SubmissionSchemaId = string
13
+
14
+ export interface SubmissionPayload<T> {
15
+ applicationId: string;
16
+ timestamp: Date;
17
+ schemaId: SubmissionSchemaId;
18
+ submission: T;
19
+ }
20
+
21
+ export interface DataSubmissionDialogMessage extends DialogMessage {
22
+ payload: SubmissionPayload<unknown>;
23
+ }
24
+
25
+ export interface WelshareConnectionOptions {
26
+ applicationId: string;
27
+ //todo: must go into build config, not supposed to be used by users
28
+ apiBaseUrl?: string;
29
+ environment?: WelshareEnvironment;
30
+ callbacks: {
31
+ onUploaded?: (payload: SubmissionPayload<unknown>) => void;
32
+ onError?: (error: string) => void;
33
+ onSessionReady?: (sessionPubKey: string) => void;
34
+ onDialogClosing?: () => void;
35
+ onDialogReady?: () => void;
36
+ };
37
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "extends": "@workspace/typescript-config/react-library.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": {
6
+ "@/*": ["./src/*"],
7
+ "@/types/*": ["./types/*"],
8
+ },
9
+ "plugins": [
10
+ ],
11
+ "allowJs": true,
12
+ "jsx": "react-jsx"
13
+ },
14
+ "include": [
15
+ "next-env.d.ts",
16
+ "next.config.ts",
17
+ "**/*.ts",
18
+ "**/*.tsx",
19
+ ],
20
+ "exclude": ["node_modules"]
21
+ }