@welshare/react 0.0.1-alpha.1 → 0.1.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.
Files changed (38) hide show
  1. package/README.md +44 -0
  2. package/dist/esm/hooks/use-welshare.d.ts +5 -0
  3. package/dist/esm/hooks/use-welshare.d.ts.map +1 -1
  4. package/dist/esm/hooks/use-welshare.js +114 -5
  5. package/dist/esm/index.d.ts +3 -0
  6. package/dist/esm/index.d.ts.map +1 -1
  7. package/dist/esm/index.js +4 -1
  8. package/dist/esm/lib/encryption.d.ts +19 -0
  9. package/dist/esm/lib/encryption.d.ts.map +1 -0
  10. package/dist/esm/lib/encryption.js +61 -0
  11. package/dist/esm/lib/uploads.d.ts +3 -0
  12. package/dist/esm/lib/uploads.d.ts.map +1 -0
  13. package/dist/esm/lib/uploads.js +17 -0
  14. package/dist/esm/types.d.ts +31 -1
  15. package/dist/esm/types.d.ts.map +1 -1
  16. package/dist/node_modules/@welshare/react/.DS_Store +0 -0
  17. package/dist/node_modules/@welshare/react/README.md +44 -0
  18. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.d.ts +5 -0
  19. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.d.ts.map +1 -1
  20. package/dist/node_modules/@welshare/react/dist/esm/hooks/use-welshare.js +114 -5
  21. package/dist/node_modules/@welshare/react/dist/esm/index.d.ts +3 -0
  22. package/dist/node_modules/@welshare/react/dist/esm/index.d.ts.map +1 -1
  23. package/dist/node_modules/@welshare/react/dist/esm/index.js +4 -1
  24. package/dist/node_modules/@welshare/react/dist/esm/lib/encryption.d.ts +19 -0
  25. package/dist/node_modules/@welshare/react/dist/esm/lib/encryption.d.ts.map +1 -0
  26. package/dist/node_modules/@welshare/react/dist/esm/lib/encryption.js +61 -0
  27. package/dist/node_modules/@welshare/react/dist/esm/lib/uploads.d.ts +3 -0
  28. package/dist/node_modules/@welshare/react/dist/esm/lib/uploads.d.ts.map +1 -0
  29. package/dist/node_modules/@welshare/react/dist/esm/lib/uploads.js +17 -0
  30. package/dist/node_modules/@welshare/react/dist/esm/types.d.ts +31 -1
  31. package/dist/node_modules/@welshare/react/dist/esm/types.d.ts.map +1 -1
  32. package/dist/node_modules/@welshare/react/package.json +1 -1
  33. package/dist/node_modules/@welshare/react/src/hooks/use-welshare.ts +154 -5
  34. package/dist/node_modules/@welshare/react/src/index.ts +13 -5
  35. package/dist/node_modules/@welshare/react/src/lib/encryption.ts +110 -0
  36. package/dist/node_modules/@welshare/react/src/lib/uploads.ts +29 -0
  37. package/dist/node_modules/@welshare/react/src/types.ts +37 -1
  38. package/package.json +1 -1
@@ -1,18 +1,22 @@
1
- import { useEffect, useState } from "react";
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { encryptAndUploadFile } from "../lib/uploads.js";
2
3
  export const useWelshare = (props) => {
3
4
  const [storageKey, setStorageKey] = useState();
4
5
  const [isDialogOpen, setIsDialogOpen] = useState(false);
5
6
  const [dialogWindow, setDialogWindow] = useState(null);
6
7
  const [messageIdCounter, setMessageIdCounter] = useState(0);
7
8
  const [isSubmitting, setIsSubmitting] = useState(false);
9
+ const [uploadState, setUploadState] = useState();
10
+ // (claude opus) Use ref to control current upload in effect without triggering re-renders
11
+ const currentUploadRef = useRef(null);
8
12
  const options = {
9
13
  environment: "development",
10
- apiBaseUrl: "https://wallet.welshare.health",
14
+ apiBaseUrl: "https://wallet.welshare.app",
11
15
  ...props,
12
16
  };
13
17
  const WELSHARE_WALLET_URL = `${options.apiBaseUrl}/wallet-external`;
14
18
  useEffect(() => {
15
- const handleMessage = (event) => {
19
+ const handleMessage = async (event) => {
16
20
  // Verify origin for security
17
21
  if (event.origin !== new URL(WELSHARE_WALLET_URL).origin) {
18
22
  return;
@@ -24,6 +28,10 @@ export const useWelshare = (props) => {
24
28
  errorMessage = message.payload.error || "An unknown error occurred";
25
29
  console.error("Welshare Wallet sent an error:", errorMessage);
26
30
  setIsSubmitting(false);
31
+ // Reject the promise if there's an ongoing upload
32
+ if (currentUploadRef.current?.uploadPromise) {
33
+ currentUploadRef.current.uploadPromise.reject(new Error(errorMessage));
34
+ }
27
35
  options.callbacks.onError?.(errorMessage);
28
36
  break;
29
37
  case "DIALOG_READY":
@@ -46,6 +54,10 @@ export const useWelshare = (props) => {
46
54
  console.debug("Welshare Wallet Dialog is closing");
47
55
  setStorageKey(undefined);
48
56
  setIsDialogOpen(false);
57
+ // Reject any pending upload promises
58
+ if (currentUploadRef.current?.uploadPromise) {
59
+ currentUploadRef.current.uploadPromise.reject(new Error("Dialog closed before upload completed"));
60
+ }
49
61
  options.callbacks.onDialogClosing?.();
50
62
  break;
51
63
  case "DATA_UPLOADED":
@@ -53,6 +65,62 @@ export const useWelshare = (props) => {
53
65
  setIsSubmitting(false);
54
66
  options.callbacks.onUploaded?.(message.payload);
55
67
  break;
68
+ //todo: make this work for batches, too
69
+ case "UPLOAD_CREDENTIALS_CREATED":
70
+ const credentials = message.payload;
71
+ console.debug("upload credentials created", credentials);
72
+ if (!currentUploadRef.current) {
73
+ throw new Error("No upload in progress");
74
+ }
75
+ if (!dialogWindow) {
76
+ currentUploadRef.current.uploadPromise.reject(new Error("(UPLOAD_CREDENTIALS_CREATED) Dialog window not open"));
77
+ return;
78
+ }
79
+ try {
80
+ setUploadState("uploading");
81
+ const encodedEncryptionKey = await encryptAndUploadFile(currentUploadRef.current.file, credentials.presignedUrl);
82
+ setUploadState("anchoring");
83
+ const binarySubmissionPayload = {
84
+ timestamp: currentUploadRef.current.timestamp,
85
+ applicationId: options.applicationId,
86
+ reference: currentUploadRef.current.reference,
87
+ fileName: currentUploadRef.current.fileName,
88
+ fileType: currentUploadRef.current.fileType,
89
+ encryptionKey: encodedEncryptionKey,
90
+ fileSize: currentUploadRef.current.file.size,
91
+ url: `welshare://${credentials.uploadKey}`,
92
+ };
93
+ //write to Nillion
94
+ const message = {
95
+ type: "SUBMIT_BINARY_DATA",
96
+ id: String(messageIdCounter),
97
+ payload: binarySubmissionPayload,
98
+ };
99
+ dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
100
+ setMessageIdCounter((prev) => prev + 1);
101
+ }
102
+ catch (error) {
103
+ console.error("error uploading file", error);
104
+ // Update status to error
105
+ currentUploadRef.current.uploadPromise.reject(error);
106
+ setUploadState("error");
107
+ // Call error callback
108
+ const errorMsg = error instanceof Error ? error.message : "Upload failed";
109
+ options.callbacks.onError?.(errorMsg);
110
+ }
111
+ break;
112
+ //{insertetUid, errors}
113
+ case "BINARY_DATA_SUBMITTED":
114
+ if (!currentUploadRef.current) {
115
+ throw new Error("No upload in progress");
116
+ }
117
+ currentUploadRef.current.uploadPromise.resolve({
118
+ url: message.payload.url,
119
+ binaryFileUid: message.payload.insertedUid,
120
+ });
121
+ setUploadState("finished");
122
+ options.callbacks.onFileUploaded?.(message.payload.insertedUid, message.payload.url);
123
+ break;
56
124
  default:
57
125
  console.log("Received unexpected message from Welshare Wallet:", message);
58
126
  }
@@ -61,14 +129,53 @@ export const useWelshare = (props) => {
61
129
  return () => {
62
130
  window.removeEventListener("message", handleMessage);
63
131
  };
64
- }, [WELSHARE_WALLET_URL, options.callbacks]);
132
+ }, [WELSHARE_WALLET_URL, dialogWindow, messageIdCounter, options.applicationId, options.callbacks]);
133
+ /**
134
+ * Starts a file upload and returns a promise that resolves with the uploaded file URL
135
+ * @param file The file to upload
136
+ * @param reference string A reference identifier for the upload
137
+ * @returns Promise<{ url: string; binaryFileUid: string }> url usually starts with welshare:// and resolves to an encrypted storage location
138
+ */
139
+ const uploadFile = (file, reference) => {
140
+ return new Promise((resolve, reject) => {
141
+ if (!dialogWindow) {
142
+ reject(new Error("(uploadFile) Dialog window not open"));
143
+ return;
144
+ }
145
+ const payload = {
146
+ timestamp: new Date(),
147
+ applicationId: options.applicationId,
148
+ reference: reference,
149
+ fileName: file.name,
150
+ fileType: file.type,
151
+ };
152
+ setUploadState("preparing");
153
+ // (claude opus) Store the upload with the file and promise handlers on a ref
154
+ currentUploadRef.current = {
155
+ ...payload,
156
+ file,
157
+ reference,
158
+ uploadPromise: { resolve, reject },
159
+ };
160
+ const message = {
161
+ type: "REQUEST_UPLOAD_CREDENTIALS",
162
+ id: String(messageIdCounter),
163
+ payload: {
164
+ ...payload,
165
+ applicationId: options.applicationId,
166
+ },
167
+ };
168
+ dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
169
+ setMessageIdCounter((prev) => prev + 1);
170
+ });
171
+ };
65
172
  /**
66
173
  * @param schemaType a welshare schema type uid
67
174
  * @param submission your submission that validates against the schema type
68
175
  */
69
176
  const submitData = (schemaId, submission) => {
70
177
  if (!dialogWindow) {
71
- throw new Error("Dialog window not open");
178
+ throw new Error("(submitData) Dialog window not open");
72
179
  }
73
180
  const submissionPayload = {
74
181
  applicationId: options.applicationId,
@@ -105,7 +212,9 @@ export const useWelshare = (props) => {
105
212
  storageKey,
106
213
  openWallet,
107
214
  isDialogOpen,
215
+ uploadFile,
108
216
  submitData,
217
+ uploadState,
109
218
  isSubmitting,
110
219
  };
111
220
  };
@@ -1,7 +1,10 @@
1
1
  export { ConnectWelshareButton } from "./components/connect-button.js";
2
2
  export { useWelshare } from "./hooks/use-welshare.js";
3
+ export { decodeEncryptionKey, decrypt, encodeEncryptionKey, encryptFile, generateRandomAESKey, type EncryptionKey } from "./lib/encryption.js";
4
+ export { encryptAndUploadFile } from "./lib/uploads.js";
3
5
  export declare const Schemas: {
4
6
  QuestionnaireResponse: string;
5
7
  ReflexSubmission: string;
8
+ BinaryFile: string;
6
9
  };
7
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAEvE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EACL,mBAAmB,EACnB,OAAO,EACP,mBAAmB,EACnB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,eAAO,MAAM,OAAO;;;;CAInB,CAAC"}
@@ -3,8 +3,11 @@
3
3
  export { ConnectWelshareButton } from "./components/connect-button.js";
4
4
  // ---- hooks ----
5
5
  export { useWelshare } from "./hooks/use-welshare.js";
6
+ export { decodeEncryptionKey, decrypt, encodeEncryptionKey, encryptFile, generateRandomAESKey } from "./lib/encryption.js";
7
+ export { encryptAndUploadFile } from "./lib/uploads.js";
6
8
  //todo: import them from the SDK or a dedicated SDK constants export
7
9
  export const Schemas = {
8
10
  QuestionnaireResponse: "b14b538f-7de3-4767-ad77-464d755d78bd", //QuestionnaireResponseSchema.schemaUid,
9
- ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c" //ReflexSubmissionSchema.schemaUid,
11
+ ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c", //ReflexSubmissionSchema.schemaUid,
12
+ BinaryFile: "9d696baf-483f-4cc0-b748-23a22c1705f5" //BinaryFilesSchema.schemaUid,
10
13
  };
@@ -0,0 +1,19 @@
1
+ export declare const ALGORITHM = "AES-GCM";
2
+ export type Algorithm = "AES-GCM";
3
+ export declare const generateRandomAESKey: () => Promise<CryptoKey>;
4
+ export declare const encryptFile: (file: File, key: CryptoKey) => Promise<{
5
+ encryptedData: ArrayBuffer;
6
+ iv: Uint8Array;
7
+ }>;
8
+ export type EncryptionKey = {
9
+ algorithm: Algorithm;
10
+ key: string;
11
+ iv: string;
12
+ };
13
+ export declare const encodeEncryptionKey: (key: CryptoKey, iv: Uint8Array) => Promise<EncryptionKey>;
14
+ export declare const decodeEncryptionKey: (encryptionKey: EncryptionKey) => {
15
+ key: Uint8Array<ArrayBuffer>;
16
+ iv: Uint8Array<ArrayBuffer>;
17
+ };
18
+ export declare const decrypt: (encryptedData: ArrayBuffer, encryptionKey: EncryptionKey) => Promise<ArrayBuffer | null>;
19
+ //# sourceMappingURL=encryption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../../../src/lib/encryption.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,YAAY,CAAC;AACnC,MAAM,MAAM,SAAS,GAAG,SAAS,CAAC;AAElC,eAAO,MAAM,oBAAoB,QAAa,OAAO,CAAC,SAAS,CAU9D,CAAC;AAIF,eAAO,MAAM,WAAW,SAChB,IAAI,OACL,SAAS,KACb,OAAO,CAAC;IAAE,aAAa,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,UAAU,CAAA;CAAE,CAgBxD,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,eAAO,MAAM,mBAAmB,QACzB,SAAS,MACV,UAAU,KACb,OAAO,CAAC,aAAa,CAgBvB,CAAC;AAEF,eAAO,MAAM,mBAAmB,kBACf,aAAa,KAC3B;IAAE,GAAG,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC;IAAC,EAAE,EAAE,UAAU,CAAC,WAAW,CAAC,CAAA;CAU7D,CAAC;AAGF,eAAO,MAAM,OAAO,kBACH,WAAW,iBACX,aAAa,KAC3B,OAAO,CAAC,WAAW,GAAG,IAAI,CA0B5B,CAAC"}
@@ -0,0 +1,61 @@
1
+ export const ALGORITHM = "AES-GCM";
2
+ export const generateRandomAESKey = async () => {
3
+ // Generate a 256-bit AES-GCM key for file encryption
4
+ return window.crypto.subtle.generateKey({
5
+ name: ALGORITHM,
6
+ length: 256, // 256-bit key
7
+ }, true, // Key is extractable (needed for storage/transmission)
8
+ ["encrypt", "decrypt"] // Key usage
9
+ );
10
+ };
11
+ /// also Generates random IV (12 bytes for AES-GCM)
12
+ /// @return {arraybuffer ciphertext, uint8array iv}
13
+ export const encryptFile = async (file, key) => {
14
+ // Read file as ArrayBuffer
15
+ const fileData = await file.arrayBuffer();
16
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
17
+ // Encrypt the file data
18
+ const encryptedData = await window.crypto.subtle.encrypt({
19
+ name: ALGORITHM,
20
+ iv: iv,
21
+ }, key, fileData);
22
+ return { encryptedData, iv };
23
+ };
24
+ export const encodeEncryptionKey = async (key, iv) => {
25
+ // Export the key as raw bytes
26
+ const exportedKey = await window.crypto.subtle.exportKey("raw", key);
27
+ const keyHex = Array.from(new Uint8Array(exportedKey))
28
+ .map((b) => b.toString(16).padStart(2, "0"))
29
+ .join("");
30
+ const ivHex = Array.from(iv)
31
+ .map((b) => b.toString(16).padStart(2, "0"))
32
+ .join("");
33
+ return {
34
+ algorithm: ALGORITHM,
35
+ key: keyHex,
36
+ iv: ivHex,
37
+ };
38
+ };
39
+ export const decodeEncryptionKey = (encryptionKey) => {
40
+ const keyBytes = new Uint8Array(encryptionKey.key
41
+ .match(/.{1,2}/g)
42
+ .map((byte) => parseInt(byte, 16)));
43
+ const ivBytes = new Uint8Array(encryptionKey.iv.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
44
+ return { key: keyBytes, iv: ivBytes };
45
+ };
46
+ // Helper function to decrypt a file using encoded encryption key
47
+ export const decrypt = async (encryptedData, encryptionKey) => {
48
+ try {
49
+ const { key: keyBytes, iv } = decodeEncryptionKey(encryptionKey);
50
+ const key = await window.crypto.subtle.importKey("raw", keyBytes, { name: ALGORITHM }, false, ["decrypt"]);
51
+ const decryptedData = await window.crypto.subtle.decrypt({
52
+ name: ALGORITHM,
53
+ iv,
54
+ }, key, encryptedData);
55
+ return decryptedData;
56
+ }
57
+ catch (error) {
58
+ console.error("Failed to decrypt file:", error);
59
+ return null;
60
+ }
61
+ };
@@ -0,0 +1,3 @@
1
+ import { EncryptionKey } from "./encryption.js";
2
+ export declare const encryptAndUploadFile: (file: File, presignedUrl: string) => Promise<EncryptionKey>;
3
+ //# sourceMappingURL=uploads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploads.d.ts","sourceRoot":"","sources":["../../../src/lib/uploads.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,aAAa,EAEd,MAAM,iBAAiB,CAAC;AAEzB,eAAO,MAAM,oBAAoB,SACzB,IAAI,gBACI,MAAM,KACnB,OAAO,CAAC,aAAa,CAkBvB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { encodeEncryptionKey, encryptFile, generateRandomAESKey, } from "./encryption.js";
2
+ export const encryptAndUploadFile = async (file, presignedUrl) => {
3
+ const encryptionKey = await generateRandomAESKey();
4
+ const { encryptedData, iv } = await encryptFile(file, encryptionKey);
5
+ // Upload encrypted file to S3
6
+ const uploadResponse = await fetch(presignedUrl, {
7
+ method: "PUT",
8
+ body: encryptedData,
9
+ headers: {
10
+ "Content-Type": file.type,
11
+ },
12
+ });
13
+ if (!uploadResponse.ok) {
14
+ throw new Error(`Failed to upload file ${uploadResponse.status}`);
15
+ }
16
+ return encodeEncryptionKey(encryptionKey, iv);
17
+ };
@@ -1,3 +1,4 @@
1
+ import { EncryptionKey } from "./lib/encryption.js";
1
2
  export interface DialogMessage {
2
3
  type: string;
3
4
  payload?: any;
@@ -14,14 +15,43 @@ export interface SubmissionPayload<T> {
14
15
  schemaId: SubmissionSchemaId;
15
16
  submission: T;
16
17
  }
18
+ export type UploadCredentials = {
19
+ presignedUrl: string;
20
+ uploadKey: string;
21
+ };
22
+ export interface RequestUploadCredentialsPayload {
23
+ timestamp?: Date;
24
+ applicationId: string;
25
+ reference: string;
26
+ fileName: string;
27
+ fileType: string;
28
+ }
29
+ export interface BinaryFileSubmissionPayload extends RequestUploadCredentialsPayload {
30
+ encryptionKey: EncryptionKey;
31
+ fileSize: number;
32
+ url: string;
33
+ }
34
+ export interface RunningFileUpload extends RequestUploadCredentialsPayload {
35
+ credentials?: UploadCredentials;
36
+ file: File;
37
+ reference: string;
38
+ uploadPromise: {
39
+ resolve: (result: {
40
+ url: string;
41
+ binaryFileUid: string;
42
+ }) => void;
43
+ reject: (error: Error) => void;
44
+ };
45
+ }
17
46
  export interface DataSubmissionDialogMessage extends DialogMessage {
18
- payload: SubmissionPayload<unknown>;
47
+ payload: SubmissionPayload<unknown> | BinaryFileSubmissionPayload | RequestUploadCredentialsPayload;
19
48
  }
20
49
  export interface WelshareConnectionOptions {
21
50
  applicationId: string;
22
51
  apiBaseUrl?: string;
23
52
  environment?: WelshareEnvironment;
24
53
  callbacks: {
54
+ onFileUploaded?: (insertedUid: string, url: string) => void;
25
55
  onUploaded?: (payload: SubmissionPayload<unknown>) => void;
26
56
  onError?: (error: string) => void;
27
57
  onSessionReady?: (sessionPubKey: string) => void;
@@ -1 +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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,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,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,MAAM,WAAW,+BAA+B;IAC9C,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,2BAA4B,SAAQ,+BAA+B;IAClF,aAAa,EAAE,aAAa,CAAC;IAE7B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAkB,SAAQ,+BAA+B;IACxE,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE;QACb,OAAO,EAAE,CAAC,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,aAAa,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAC;QAClE,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;KAChC,CAAC;CACH;AAED,MAAM,WAAW,2BAA4B,SAAQ,aAAa;IAChE,OAAO,EACH,iBAAiB,CAAC,OAAO,CAAC,GAC1B,2BAA2B,GAC3B,+BAA+B,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,cAAc,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5D,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"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@welshare/react",
3
- "version": "0.0.1-alpha.1",
3
+ "version": "0.1.0",
4
4
  "description": "React library for integrating with Welshare's sovereign data sharing platform",
5
5
  "keywords": [
6
6
  "react",
@@ -1,10 +1,15 @@
1
1
  import {
2
+ BinaryFileSubmissionPayload,
2
3
  DialogMessage,
4
+ RequestUploadCredentialsPayload,
5
+ RunningFileUpload,
3
6
  SubmissionPayload,
4
7
  SubmissionSchemaId,
8
+ UploadCredentials,
5
9
  WelshareConnectionOptions,
6
10
  } from "@/types.js";
7
- import { useEffect, useState } from "react";
11
+ import { useEffect, useRef, useState } from "react";
12
+ import { encryptAndUploadFile } from "../lib/uploads.js";
8
13
 
9
14
  export const useWelshare = (props: WelshareConnectionOptions) => {
10
15
  const [storageKey, setStorageKey] = useState<string>();
@@ -12,17 +17,23 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
12
17
  const [dialogWindow, setDialogWindow] = useState<Window | null>(null);
13
18
  const [messageIdCounter, setMessageIdCounter] = useState(0);
14
19
  const [isSubmitting, setIsSubmitting] = useState(false);
20
+ const [uploadState, setUploadState] = useState<
21
+ "preparing" | "uploading" | "anchoring" | "finished" | "error"
22
+ >();
23
+
24
+ // (claude opus) Use ref to control current upload in effect without triggering re-renders
25
+ const currentUploadRef = useRef<RunningFileUpload>(null);
15
26
 
16
27
  const options: WelshareConnectionOptions = {
17
28
  environment: "development",
18
- apiBaseUrl: "https://wallet.welshare.health",
29
+ apiBaseUrl: "https://wallet.welshare.app",
19
30
  ...props,
20
31
  };
21
32
 
22
33
  const WELSHARE_WALLET_URL = `${options.apiBaseUrl}/wallet-external`;
23
34
 
24
35
  useEffect(() => {
25
- const handleMessage = (event: MessageEvent<DialogMessage>) => {
36
+ const handleMessage = async (event: MessageEvent<DialogMessage>) => {
26
37
  // Verify origin for security
27
38
  if (event.origin !== new URL(WELSHARE_WALLET_URL).origin) {
28
39
  return;
@@ -35,6 +46,14 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
35
46
  errorMessage = message.payload.error || "An unknown error occurred";
36
47
  console.error("Welshare Wallet sent an error:", errorMessage);
37
48
  setIsSubmitting(false);
49
+
50
+ // Reject the promise if there's an ongoing upload
51
+ if (currentUploadRef.current?.uploadPromise) {
52
+ currentUploadRef.current.uploadPromise.reject(
53
+ new Error(errorMessage)
54
+ );
55
+ }
56
+
38
57
  options.callbacks.onError?.(errorMessage);
39
58
  break;
40
59
  case "DIALOG_READY":
@@ -61,6 +80,14 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
61
80
  console.debug("Welshare Wallet Dialog is closing");
62
81
  setStorageKey(undefined);
63
82
  setIsDialogOpen(false);
83
+
84
+ // Reject any pending upload promises
85
+ if (currentUploadRef.current?.uploadPromise) {
86
+ currentUploadRef.current.uploadPromise.reject(
87
+ new Error("Dialog closed before upload completed")
88
+ );
89
+ }
90
+
64
91
  options.callbacks.onDialogClosing?.();
65
92
  break;
66
93
 
@@ -69,6 +96,79 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
69
96
  setIsSubmitting(false);
70
97
  options.callbacks.onUploaded?.(message.payload);
71
98
  break;
99
+
100
+ //todo: make this work for batches, too
101
+ case "UPLOAD_CREDENTIALS_CREATED":
102
+ const credentials: UploadCredentials = message.payload;
103
+ console.debug("upload credentials created", credentials);
104
+
105
+ if (!currentUploadRef.current) {
106
+ throw new Error("No upload in progress");
107
+ }
108
+
109
+ if (!dialogWindow) {
110
+ currentUploadRef.current.uploadPromise.reject(
111
+ new Error("(UPLOAD_CREDENTIALS_CREATED) Dialog window not open")
112
+ );
113
+ return;
114
+ }
115
+ try {
116
+ setUploadState("uploading");
117
+ const encodedEncryptionKey = await encryptAndUploadFile(
118
+ currentUploadRef.current.file,
119
+ credentials.presignedUrl
120
+ );
121
+
122
+ setUploadState("anchoring");
123
+ const binarySubmissionPayload: BinaryFileSubmissionPayload = {
124
+ timestamp: currentUploadRef.current.timestamp,
125
+ applicationId: options.applicationId,
126
+ reference: currentUploadRef.current.reference,
127
+ fileName: currentUploadRef.current.fileName,
128
+ fileType: currentUploadRef.current.fileType,
129
+ encryptionKey: encodedEncryptionKey,
130
+ fileSize: currentUploadRef.current.file.size,
131
+ url: `welshare://${credentials.uploadKey}`,
132
+ };
133
+
134
+ //write to Nillion
135
+ const message: DialogMessage = {
136
+ type: "SUBMIT_BINARY_DATA",
137
+ id: String(messageIdCounter),
138
+ payload: binarySubmissionPayload,
139
+ };
140
+
141
+ dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
142
+ setMessageIdCounter((prev) => prev + 1);
143
+ } catch (error) {
144
+ console.error("error uploading file", error);
145
+ // Update status to error
146
+ currentUploadRef.current.uploadPromise.reject(error as Error);
147
+ setUploadState("error");
148
+
149
+ // Call error callback
150
+ const errorMsg =
151
+ error instanceof Error ? error.message : "Upload failed";
152
+ options.callbacks.onError?.(errorMsg);
153
+ }
154
+ break;
155
+ //{insertetUid, errors}
156
+ case "BINARY_DATA_SUBMITTED":
157
+ if (!currentUploadRef.current) {
158
+ throw new Error("No upload in progress");
159
+ }
160
+ currentUploadRef.current.uploadPromise.resolve({
161
+ url: message.payload.url,
162
+ binaryFileUid: message.payload.insertedUid,
163
+ });
164
+
165
+ setUploadState("finished");
166
+ options.callbacks.onFileUploaded?.(
167
+ message.payload.insertedUid,
168
+ message.payload.url
169
+ );
170
+ break;
171
+
72
172
  default:
73
173
  console.log(
74
174
  "Received unexpected message from Welshare Wallet:",
@@ -82,7 +182,54 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
82
182
  return () => {
83
183
  window.removeEventListener("message", handleMessage);
84
184
  };
85
- }, [WELSHARE_WALLET_URL, options.callbacks]);
185
+ }, [WELSHARE_WALLET_URL, dialogWindow, messageIdCounter, options.applicationId, options.callbacks]);
186
+
187
+ /**
188
+ * Starts a file upload and returns a promise that resolves with the uploaded file URL
189
+ * @param file The file to upload
190
+ * @param reference string A reference identifier for the upload
191
+ * @returns Promise<{ url: string; binaryFileUid: string }> url usually starts with welshare:// and resolves to an encrypted storage location
192
+ */
193
+ const uploadFile = (
194
+ file: File,
195
+ reference: string
196
+ ): Promise<{ url: string; binaryFileUid: string }> => {
197
+ return new Promise((resolve, reject) => {
198
+ if (!dialogWindow) {
199
+ reject(new Error("(uploadFile) Dialog window not open"));
200
+ return;
201
+ }
202
+
203
+ const payload: RequestUploadCredentialsPayload = {
204
+ timestamp: new Date(),
205
+ applicationId: options.applicationId,
206
+ reference: reference,
207
+ fileName: file.name,
208
+ fileType: file.type,
209
+ };
210
+
211
+ setUploadState("preparing");
212
+ // (claude opus) Store the upload with the file and promise handlers on a ref
213
+ currentUploadRef.current = {
214
+ ...payload,
215
+ file,
216
+ reference,
217
+ uploadPromise: { resolve, reject },
218
+ };
219
+
220
+ const message: DialogMessage = {
221
+ type: "REQUEST_UPLOAD_CREDENTIALS",
222
+ id: String(messageIdCounter),
223
+ payload: {
224
+ ...payload,
225
+ applicationId: options.applicationId,
226
+ },
227
+ };
228
+
229
+ dialogWindow.postMessage(message, WELSHARE_WALLET_URL);
230
+ setMessageIdCounter((prev) => prev + 1);
231
+ });
232
+ };
86
233
 
87
234
  /**
88
235
  * @param schemaType a welshare schema type uid
@@ -93,7 +240,7 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
93
240
  submission: SubmissionPayload<T>
94
241
  ) => {
95
242
  if (!dialogWindow) {
96
- throw new Error("Dialog window not open");
243
+ throw new Error("(submitData) Dialog window not open");
97
244
  }
98
245
 
99
246
  const submissionPayload = {
@@ -142,7 +289,9 @@ export const useWelshare = (props: WelshareConnectionOptions) => {
142
289
  storageKey,
143
290
  openWallet,
144
291
  isDialogOpen,
292
+ uploadFile,
145
293
  submitData,
294
+ uploadState,
146
295
  isSubmitting,
147
296
  };
148
297
  };
@@ -2,12 +2,20 @@
2
2
  //import { QuestionnaireResponseSchema, ReflexSubmissionSchema } from "@welshare/sdk";
3
3
  export { ConnectWelshareButton } from "./components/connect-button.js";
4
4
  // ---- hooks ----
5
- export {
6
- useWelshare
7
- } from "./hooks/use-welshare.js";
5
+ export { useWelshare } from "./hooks/use-welshare.js";
8
6
 
7
+ export {
8
+ decodeEncryptionKey,
9
+ decrypt,
10
+ encodeEncryptionKey,
11
+ encryptFile,
12
+ generateRandomAESKey,
13
+ type EncryptionKey
14
+ } from "./lib/encryption.js";
15
+ export { encryptAndUploadFile } from "./lib/uploads.js";
9
16
  //todo: import them from the SDK or a dedicated SDK constants export
10
17
  export const Schemas = {
11
18
  QuestionnaireResponse: "b14b538f-7de3-4767-ad77-464d755d78bd", //QuestionnaireResponseSchema.schemaUid,
12
- ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c" //ReflexSubmissionSchema.schemaUid,
13
- };
19
+ ReflexSubmission: "f5cf2d8a-1f78-4f21-b4bd-082e983b830c", //ReflexSubmissionSchema.schemaUid,
20
+ BinaryFile: "9d696baf-483f-4cc0-b748-23a22c1705f5" //BinaryFilesSchema.schemaUid,
21
+ };