dome-embedded-app-sdk 0.1.2
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 +64 -0
- package/package.json +38 -0
- package/src/crypto.ts +133 -0
- package/src/dome-sdk.ts +778 -0
- package/src/index.ts +9 -0
- package/src/types/globals.d.ts +20 -0
- package/src/utils.ts +30 -0
- package/tsconfig.json +25 -0
- package/webpack.config.ts +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# The Official Dome SDK
|
|
2
|
+
|
|
3
|
+
Use this SDK to build plugins for Dome. There are two plugins supported:
|
|
4
|
+
|
|
5
|
+
1. Cards : Extend the functionality of Dome by adding custom cards.
|
|
6
|
+
|
|
7
|
+
2. Document Viewer : Add support for editing & viewing any document in Dome.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## 1. Cards
|
|
11
|
+
|
|
12
|
+
A "card" in Dome enables you to extend the functionality of a dome. Each "card" is like a mini website (webapp) that you can build as per your needs. Each dome is made up of cards. By adding your custom card to your dome, you can extend it's functionality.
|
|
13
|
+
|
|
14
|
+
Use this SDK to create your custom card that can do whatever you like: a simple todo list, snake & ladder game, to a complex project management tool, a 3D AR visualization, to anything else you can imagine! Anything you can build in React or Angular can be created into a card!
|
|
15
|
+
|
|
16
|
+
As of Dec 2024, we support Angular and React cards. In future, we will add support for more frameworks.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Getting Started with your first Card
|
|
20
|
+
|
|
21
|
+
#### Register your card
|
|
22
|
+
Register your card at https://dev.dome.so
|
|
23
|
+
|
|
24
|
+
#### Code
|
|
25
|
+
Import the SDK
|
|
26
|
+
```
|
|
27
|
+
import { CardSdk } from "dome-embedded-app-sdk";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Call init to get instance of the SDK. It takes a secret and event handler as input.
|
|
31
|
+
```
|
|
32
|
+
CardSdk.init(my_dev_card_secret, {
|
|
33
|
+
onInit: (data: any) => {
|
|
34
|
+
this.user = data?.user;
|
|
35
|
+
this.api_token = data?.api_token;
|
|
36
|
+
},
|
|
37
|
+
onError: (error_code: string | number, message: string, data: any) => {
|
|
38
|
+
console.error("Some Error", message + "(" + error_code + ")");
|
|
39
|
+
},
|
|
40
|
+
onRefreshRequest: (data: any) => {
|
|
41
|
+
console.debug("Refresh requested", data);
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
Note: the `secret` is given to you when you register your card (step 1)
|
|
46
|
+
|
|
47
|
+
### Deploy
|
|
48
|
+
Deploy your code at https://dev.dome.so
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## 2. Document Viewer
|
|
52
|
+
|
|
53
|
+
Add view / edit capability for any document in Dome. For example, you can come up with your own spreadsheet and make it instantly available to all users of Dome. Alternatively, you can create your own viewer that views & (optionally) edits existing documents such as Excel files. The options a limitless!
|
|
54
|
+
|
|
55
|
+
As of Dec 2024, we support Angular and React document viewers. In future, we will add support for more frameworks.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
### Getting Started with your first Document Viewer
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Help
|
|
63
|
+
|
|
64
|
+
Join our developer community on Dome here: <url>. Hang out, get latest updates, ask questions, experience Dome, and much more!
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dome-embedded-app-sdk",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"source": "src/index.ts",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"require": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build:tsc": "tsc --emitDeclarationOnly",
|
|
15
|
+
"build:webpack": "webpack --config webpack.config.ts",
|
|
16
|
+
"build": "npm run build:tsc && npm run build:webpack",
|
|
17
|
+
"release": "npm run build && npm publish",
|
|
18
|
+
"watch": "webpack --watch"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"dome",
|
|
25
|
+
"dome-sdk"
|
|
26
|
+
],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"description": "",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.13.10",
|
|
32
|
+
"@types/webpack": "^5.28.5",
|
|
33
|
+
"ts-loader": "^9.5.1",
|
|
34
|
+
"ts-node": "^10.9.2",
|
|
35
|
+
"typescript": "^5.8.2",
|
|
36
|
+
"webpack-cli": "^5.1.4"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
|
|
2
|
+
// Provide enc / dec using Algorithm01
|
|
3
|
+
export class CryptoA01 {
|
|
4
|
+
private subtleCrypto: SubtleCrypto;
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
// Initialize subtleCrypto once
|
|
8
|
+
this.subtleCrypto = window.crypto?.subtle;
|
|
9
|
+
if (!this.subtleCrypto) {
|
|
10
|
+
throw new Error('SubtleCrypto API is not available in this environment.');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Perform decryption using AES based V1 algorithm.
|
|
16
|
+
*
|
|
17
|
+
* string: the encrypted string (base64 encoded)
|
|
18
|
+
* password: the password used for encryption
|
|
19
|
+
* salt: the base64 encoded salt used
|
|
20
|
+
*/
|
|
21
|
+
public async decrypt(token: string, password: string, salt: string): Promise<string> {
|
|
22
|
+
try {
|
|
23
|
+
|
|
24
|
+
if (!token) {
|
|
25
|
+
throw new Error("Invalid token");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tokenBytes = this.base64UrlDecode(token);
|
|
29
|
+
|
|
30
|
+
// Extract token components
|
|
31
|
+
const version = tokenBytes[0];
|
|
32
|
+
if (version !== 0x80) {
|
|
33
|
+
// console.log("Incorrect Version: ", version);
|
|
34
|
+
throw new Error('Invalid version');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const timestamp = tokenBytes.slice(1, 9);
|
|
38
|
+
const iv = tokenBytes.slice(9, 25);
|
|
39
|
+
const ciphertext = tokenBytes.slice(25, -32);
|
|
40
|
+
const hmacFromToken = tokenBytes.slice(-32);
|
|
41
|
+
|
|
42
|
+
// Derive the key and split it into HMAC and AES keys
|
|
43
|
+
const fullKey = await this.deriveKey(password, salt);
|
|
44
|
+
const { hmacKey, aesKey } = await this.splitKey(fullKey);
|
|
45
|
+
|
|
46
|
+
// Compute HMAC over version + timestamp + IV + ciphertext
|
|
47
|
+
const hmacInput = tokenBytes.slice(0, -32);
|
|
48
|
+
const computedHmac = new Uint8Array(await this.subtleCrypto.sign('HMAC', hmacKey, hmacInput));
|
|
49
|
+
|
|
50
|
+
// Validate HMAC
|
|
51
|
+
if (!computedHmac.every((byte, i) => byte === hmacFromToken[i])) {
|
|
52
|
+
throw new Error('Invalid HMAC. Token has been tampered with!');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Decrypt the ciphertext
|
|
56
|
+
const decrypted = await this.subtleCrypto.decrypt(
|
|
57
|
+
{
|
|
58
|
+
name: 'AES-CBC',
|
|
59
|
+
iv: iv,
|
|
60
|
+
},
|
|
61
|
+
aesKey,
|
|
62
|
+
ciphertext
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Convert decrypted data to UTF-8 string
|
|
66
|
+
const decoder = new TextDecoder();
|
|
67
|
+
return decoder.decode(decrypted);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log("Error in decrypt:", err);
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
private async deriveKey(password: string, salt: string, iterations: number=10000): Promise<CryptoKey> {
|
|
76
|
+
const encoder = new TextEncoder();
|
|
77
|
+
const keyMaterial = await this.subtleCrypto.importKey(
|
|
78
|
+
"raw",
|
|
79
|
+
encoder.encode(password),
|
|
80
|
+
"PBKDF2",
|
|
81
|
+
false,
|
|
82
|
+
["deriveKey"]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return this.subtleCrypto.deriveKey(
|
|
86
|
+
{
|
|
87
|
+
name: "PBKDF2",
|
|
88
|
+
hash: "SHA-256",
|
|
89
|
+
salt: encoder.encode(salt),
|
|
90
|
+
iterations: iterations,
|
|
91
|
+
},
|
|
92
|
+
keyMaterial,
|
|
93
|
+
{ name: "AES-CBC", length: 256 },
|
|
94
|
+
true, // Allow export of the derived key (req for splitting)
|
|
95
|
+
["encrypt", "decrypt"]
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
// Split the full key into HMAC and AES keys
|
|
101
|
+
private async splitKey(fullKey: CryptoKey): Promise<{ hmacKey: CryptoKey; aesKey: CryptoKey }> {
|
|
102
|
+
const rawKey = new Uint8Array(await this.subtleCrypto.exportKey('raw', fullKey));
|
|
103
|
+
|
|
104
|
+
// Split the key into HMAC (first 16 bytes) and AES (last 16 bytes)
|
|
105
|
+
const hmacKey = await this.subtleCrypto.importKey(
|
|
106
|
+
'raw',
|
|
107
|
+
rawKey.slice(0, 16),
|
|
108
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
109
|
+
false,
|
|
110
|
+
['sign', 'verify']
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const aesKey = await this.subtleCrypto.importKey(
|
|
114
|
+
'raw',
|
|
115
|
+
rawKey.slice(16),
|
|
116
|
+
{ name: 'AES-CBC' },
|
|
117
|
+
false,
|
|
118
|
+
['encrypt', 'decrypt']
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return { hmacKey, aesKey };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
// Decode Base64 URL-safe strings
|
|
126
|
+
private base64UrlDecode(base64: string): Uint8Array {
|
|
127
|
+
// assumes URL safe encoding that has + in place of - and _ in place of /
|
|
128
|
+
const base64String = base64.replace(/-/g, '+').replace(/_/g, '/');
|
|
129
|
+
const decodedString = atob(base64String);
|
|
130
|
+
return new Uint8Array([...decodedString].map(c => c.charCodeAt(0)));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
}
|
package/src/dome-sdk.ts
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import pkg from "../package.json";
|
|
2
|
+
|
|
3
|
+
import { generateUUID, getNameString } from './utils'
|
|
4
|
+
import { CryptoA01 } from './crypto'
|
|
5
|
+
|
|
6
|
+
// Enum defining message types sent from the viewer to the parent application
|
|
7
|
+
export enum ViewerMessageType {
|
|
8
|
+
CONNECTION_SUCCESS = "CONNECTION_SUCCESS",
|
|
9
|
+
INIT = "INIT", // Indicates the app is initialized
|
|
10
|
+
REQUEST_SAVE = "REQUEST_SAVE", // Request to save data in the parent
|
|
11
|
+
REQUEST_CLOSE = "REQUEST_CLOSE", // Request to save data in the parent
|
|
12
|
+
REQUEST_INITIAL_DATA = "REQUEST_INITIAL_DATA", // Request to load initial data from the parent
|
|
13
|
+
SET_DIRTY = "SET_DIRTY", // Indicates the app has unsaved changes
|
|
14
|
+
SEND_CLOSE = "SEND_CLOSE", // Signals the app is ready to close
|
|
15
|
+
SEND_EXCEPTION = "SEND_EXCEPTION" // Sends an exception event to parent
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Enum defining message types sent from the parent application to the embedded app
|
|
19
|
+
enum ClientMessageType {
|
|
20
|
+
CONNECT = "CONNECT",
|
|
21
|
+
REQUEST_CLOSE = "REQUEST_CLOSE", // Requests the app to close
|
|
22
|
+
REQUEST_SAVE = "REQUEST_SAVE", // Requests the app for data to be saved
|
|
23
|
+
SAVE_ERROR = "SAVE_ERROR", // Sends status of the save event
|
|
24
|
+
SAVE_SUCCESS = "SAVE_SUCCESS", // Sends status of the save event
|
|
25
|
+
DATA_CHANGE = "DATA_CHANGE",
|
|
26
|
+
INIT_ACK = "INIT_ACK",
|
|
27
|
+
ERROR = "ERROR",
|
|
28
|
+
REFRESH = "REFRESH",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// Interface defining the structure of a message from the parent
|
|
33
|
+
interface ClientMessageEvent {
|
|
34
|
+
type: ClientMessageType; // Message type
|
|
35
|
+
data: any; // Payload data for the message
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SaveStatusData {
|
|
39
|
+
status: 'success' | 'error';
|
|
40
|
+
message: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Interface defining handlers for different message types received from the parent
|
|
44
|
+
export interface ViewerEventHandler {
|
|
45
|
+
onInitialData: (data: { doc: Document, ui: UiProps, isNewFile: boolean, perms: any, config?: Record<string, any> }) => void;
|
|
46
|
+
onDataChange?: (data: { doc: Document, perms: any, userConsent: "override" | null }) => void;
|
|
47
|
+
onCloseRequest: () => void;
|
|
48
|
+
onSaveRequest: () => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Interface defining the structure for the doc object
|
|
52
|
+
export interface Document {
|
|
53
|
+
data: any;
|
|
54
|
+
name: string;
|
|
55
|
+
type: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Interface defining the UI properties
|
|
59
|
+
export interface UiProps {
|
|
60
|
+
theme: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ALLOWED_ORIGINS = new Set(getAllowedOrigins());
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
function getAllowedOrigins(): string[] {
|
|
67
|
+
if (typeof window === 'undefined') return [];
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
window.location.origin,
|
|
71
|
+
'https://dome.so',
|
|
72
|
+
'https://spaces.intouchapp.com/',
|
|
73
|
+
'http://localhost:4200',
|
|
74
|
+
'http://localhost:4201',
|
|
75
|
+
'null',
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* DomeEmbeddedAppSdk:
|
|
81
|
+
* Base SDK class providing methods to send messages to the parent application.
|
|
82
|
+
*/
|
|
83
|
+
class DomeEmbeddedAppSdk {
|
|
84
|
+
private readonly targetOrigin: string = "*";
|
|
85
|
+
protected isAppReady: boolean = false;
|
|
86
|
+
protected port2: MessagePort | null = null;
|
|
87
|
+
private platform: "android" | "ios" | "web" | "unknown" = "unknown"; // Store detected platform
|
|
88
|
+
|
|
89
|
+
constructor() {
|
|
90
|
+
this.detectPlatform();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Detects the platform (iOS, Android, or Web) and saves it.
|
|
96
|
+
*/
|
|
97
|
+
private detectPlatform(): void {
|
|
98
|
+
if (typeof window.AndroidBridge !== "undefined") {
|
|
99
|
+
this.platform = "android";
|
|
100
|
+
} else if (typeof window.webkit !== "undefined") {
|
|
101
|
+
this.platform = "ios";
|
|
102
|
+
} else {
|
|
103
|
+
this.platform = "web";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.debug(`Detected platform: ${this.platform}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Method to send messages to the parent application.
|
|
112
|
+
* Ensures the parent window exists and sends a structured message with type and data.
|
|
113
|
+
* @param type - The type of message being sent
|
|
114
|
+
* @param data - (Optional) payload data for the message
|
|
115
|
+
*/
|
|
116
|
+
protected sendMessage(type: ViewerMessageType | CardMessageType, data?: any): void {
|
|
117
|
+
const message = { type, data: data ?? null };
|
|
118
|
+
|
|
119
|
+
switch (this.platform) {
|
|
120
|
+
case "android":
|
|
121
|
+
window.AndroidBridge?.sendMessage(JSON.stringify(message));
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case "ios":
|
|
125
|
+
if (window?.webkit?.messageHandlers) {
|
|
126
|
+
window.webkit?.messageHandlers.appHandler.postMessage(JSON.stringify(message));
|
|
127
|
+
} else {
|
|
128
|
+
console.error("webkit.messageHandlers not found")
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case "web":
|
|
133
|
+
if (this.port2) {
|
|
134
|
+
this.port2.postMessage(message);
|
|
135
|
+
} else {
|
|
136
|
+
console.error("Web connection is not established.");
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
console.error("Unsupported platform, cannot send message.");
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.debug(`Sent message to ${this.platform}:`, message);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Notifies the parent application that the app is ready, if it hasn’t already.
|
|
150
|
+
*/
|
|
151
|
+
protected sendAppInit(): void {
|
|
152
|
+
if (!this.isAppReady) {
|
|
153
|
+
this.isAppReady = true;
|
|
154
|
+
this.sendMessage(ViewerMessageType.INIT, { sdk: { ver: pkg.version } })
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Safely invokes a function from the handler object if it exists.
|
|
160
|
+
* and logs a warning if the handler is not provided for the given message type.
|
|
161
|
+
*
|
|
162
|
+
* @param eventName - Name of the event method to be invoked from the handler.
|
|
163
|
+
* @param handlerObj - The handler object that contains the message handling methods.
|
|
164
|
+
* @param data - (Optional) The data to be passed to the handler function if invoked.
|
|
165
|
+
*/
|
|
166
|
+
protected safeInvoke<T extends ViewerEventHandler | CardEventHandler>(eventName: keyof T, handlerObj: T, data?: any): void {
|
|
167
|
+
const handler = handlerObj[eventName];
|
|
168
|
+
|
|
169
|
+
if (typeof handler === 'function') {
|
|
170
|
+
handler(data);
|
|
171
|
+
} else {
|
|
172
|
+
console.warn(`Handler for '${String(eventName)}' is not defined.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
// Sets up connection with iframe parent using message channel
|
|
178
|
+
// Call this once only in the lifetime. Should be called
|
|
179
|
+
// _before_ iFrame's onLoad is called (otherwise messaging will
|
|
180
|
+
// not be setup)
|
|
181
|
+
protected setupParentConnection(): Promise<void> {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
switch (this.platform) {
|
|
184
|
+
case "android":
|
|
185
|
+
window.receiveFromAndroid = (message: { type: string; data: any }) => {
|
|
186
|
+
console.debug("Message received from Android:", message);
|
|
187
|
+
this.handleMessage(message.type, message.data);
|
|
188
|
+
};
|
|
189
|
+
resolve();
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
case "ios":
|
|
193
|
+
window.receiveFromIOS = (message: { type: string; data: any }) => {
|
|
194
|
+
console.debug("Message received from iOS:", message);
|
|
195
|
+
this.handleMessage(message.type, message.data);
|
|
196
|
+
};
|
|
197
|
+
resolve();
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case "web":
|
|
201
|
+
if (this.port2) {
|
|
202
|
+
console.warn("Connection already established. Skipping reinitialization.");
|
|
203
|
+
resolve();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const handleMessage = (event: MessageEvent) => {
|
|
207
|
+
const { type } = event.data || {};
|
|
208
|
+
|
|
209
|
+
if (type !== ClientMessageType.CONNECT) return;
|
|
210
|
+
|
|
211
|
+
if (event.ports && event.ports.length > 0) {
|
|
212
|
+
this.port2 = event.ports[0];
|
|
213
|
+
this.port2.onmessage = (e) => this.handlePortMessage(e);
|
|
214
|
+
window.removeEventListener("message", handleMessage); // Cleanup
|
|
215
|
+
this.notifyConnectionSuccess();
|
|
216
|
+
resolve();
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Listen for browser-based `message` events
|
|
221
|
+
window.addEventListener("message", handleMessage);
|
|
222
|
+
break;
|
|
223
|
+
|
|
224
|
+
default:
|
|
225
|
+
console.error("Unknown platform.");
|
|
226
|
+
reject("Unknown platform");
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
// Send CONNECTION_SUCCESS message to parent
|
|
233
|
+
private notifyConnectionSuccess(): void {
|
|
234
|
+
this.sendMessage(ViewerMessageType.CONNECTION_SUCCESS);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handle messages coming over message channel port
|
|
238
|
+
private handlePortMessage(event: MessageEvent) {
|
|
239
|
+
const { type, data } = event.data || {};
|
|
240
|
+
if (!type) return;
|
|
241
|
+
|
|
242
|
+
// Delegate to subclass-specific message handler
|
|
243
|
+
this.handleMessage(type, data);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Common method for handling messages to be implemented by sub-classes
|
|
247
|
+
protected handleMessage(type: string, data: any): void {
|
|
248
|
+
throw new Error("Subclasses must implement handleMessage.");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* ViewerSdk:
|
|
255
|
+
* A subclass of DomeEmbeddedAppSdk specifically for document viewer applications.
|
|
256
|
+
* It includes additional methods and properties to manage app interactions.
|
|
257
|
+
*/
|
|
258
|
+
export class ViewerSdk extends DomeEmbeddedAppSdk {
|
|
259
|
+
|
|
260
|
+
private static instance: ViewerSdk; // Singleton instance of ViewerSdk
|
|
261
|
+
private static initialized = false;
|
|
262
|
+
private handler: ViewerEventHandler | null = null; // Handler instance for client messages
|
|
263
|
+
private pendingRequests: Map<string, (status: SaveStatusData) => void> = new Map();
|
|
264
|
+
private pendingInitAck: unknown | null = null;
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
private constructor() {
|
|
268
|
+
super();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Static initialization method to get or create the singleton instance of ViewerSdk.
|
|
274
|
+
* Allows setting the handler during initialization.
|
|
275
|
+
* @param handler - (Optional) Custom handler for different message types
|
|
276
|
+
* @returns The singleton ViewerSdk instance
|
|
277
|
+
*/
|
|
278
|
+
static init(handler?: ViewerEventHandler): ViewerSdk {
|
|
279
|
+
console.debug("init called", handler && "with handler");
|
|
280
|
+
// Prevent reinitialization if already initialized
|
|
281
|
+
if (ViewerSdk.initialized) {
|
|
282
|
+
console.warn("ViewerSdk is already initialized. Skipping initialization.");
|
|
283
|
+
return ViewerSdk.instance;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!ViewerSdk.instance) {
|
|
287
|
+
ViewerSdk.instance = new ViewerSdk();
|
|
288
|
+
|
|
289
|
+
// Initialize parent communication - REQUIRED!
|
|
290
|
+
ViewerSdk.instance.setupParentConnection()
|
|
291
|
+
.then(() => {
|
|
292
|
+
try {
|
|
293
|
+
// Connection established with parent
|
|
294
|
+
ViewerSdk.instance.initializeViewerSdk();
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.error("Error in initializeViewerSdk:", err);
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
.catch((err: any) => {
|
|
300
|
+
console.error("init: Error setting up parent connection!", err);
|
|
301
|
+
console.trace("called from:")
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (handler) {
|
|
306
|
+
ViewerSdk.instance.setHandler(handler); // Set handler if provided during initialization
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Mark as initialized
|
|
310
|
+
ViewerSdk.initialized = true;
|
|
311
|
+
|
|
312
|
+
return ViewerSdk.instance;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Method to set or update the handler object.
|
|
318
|
+
* @param handler - Custom handler for different message types
|
|
319
|
+
*/
|
|
320
|
+
setHandler(handler: ViewerEventHandler): void {
|
|
321
|
+
this.handler = handler;
|
|
322
|
+
|
|
323
|
+
// If INIT_ACK message was received and stored, process it now
|
|
324
|
+
if (this.pendingInitAck) {
|
|
325
|
+
console.debug("Processing pending INIT_ACK message after handler is set.");
|
|
326
|
+
this.safeInvoke("onInitialData", this.handler, this.pendingInitAck);
|
|
327
|
+
this.pendingInitAck = null; // Clear the stored message
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Checks if the given permissions string allows reading.
|
|
334
|
+
* @param perms - The permissions string.
|
|
335
|
+
* @returns - True if the permission string includes read access.
|
|
336
|
+
*/
|
|
337
|
+
canRead(perms?: string): boolean {
|
|
338
|
+
return !!perms?.includes('r');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Checks if the given permissions string allows writing.
|
|
343
|
+
* @param perms - The permissions string.
|
|
344
|
+
* @returns - True if the permission string includes write access.
|
|
345
|
+
*/
|
|
346
|
+
canWrite(perms?: string): boolean {
|
|
347
|
+
return !!perms?.includes('w') || !!perms?.includes('*');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
// Initializes the viewer SDK, setting up the message listener and sending an initial "ready" message.
|
|
352
|
+
initializeViewerSdk() {
|
|
353
|
+
console.debug("initializing viewer sdk");
|
|
354
|
+
this.sendAppInit();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Sends a request to the parent application to retrieve initial data.
|
|
359
|
+
*/
|
|
360
|
+
requestInitialData(): void {
|
|
361
|
+
this.sendMessage(ViewerMessageType.REQUEST_INITIAL_DATA);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Sends a request to the parent application to save data.
|
|
367
|
+
* @param doc - payload data to be saved
|
|
368
|
+
* @param isDataDirty - Boolean indicating indicating modified data
|
|
369
|
+
*/
|
|
370
|
+
requestSave(doc: Document, isDataDirty: boolean): Promise<SaveStatusData> {
|
|
371
|
+
const requestId = generateUUID();
|
|
372
|
+
|
|
373
|
+
// Send save request with the generated requestId
|
|
374
|
+
this.sendMessage(ViewerMessageType.REQUEST_SAVE, { doc, isDataDirty, requestId });
|
|
375
|
+
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
|
|
378
|
+
this.pendingRequests.set(requestId, resolve);
|
|
379
|
+
this.pendingRequests.set(requestId + '_reject', reject);
|
|
380
|
+
|
|
381
|
+
// Timeout if the parent fails to respond in time
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
if (this.pendingRequests.has(requestId)) {
|
|
384
|
+
this.pendingRequests.delete(requestId);
|
|
385
|
+
this.pendingRequests.delete(requestId + '_reject');
|
|
386
|
+
}
|
|
387
|
+
}, 30000);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private handleOnSave(data: { requestId: string, status: "error" | "success", message: string }) {
|
|
392
|
+
const { requestId, status, message } = data;
|
|
393
|
+
|
|
394
|
+
// Check if we have a pending request for this requestId
|
|
395
|
+
const resolve = this.pendingRequests.get(requestId);
|
|
396
|
+
const reject = this.pendingRequests.get(requestId + '_reject');
|
|
397
|
+
|
|
398
|
+
if (resolve) {
|
|
399
|
+
// If status is "error", reject the promise, otherwise resolve it
|
|
400
|
+
if (status === "error") {
|
|
401
|
+
reject?.({ status, message });
|
|
402
|
+
} else {
|
|
403
|
+
resolve({ status, message });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Clean up
|
|
407
|
+
this.pendingRequests.delete(requestId);
|
|
408
|
+
this.pendingRequests.delete(requestId + '_reject');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Sets the viewer's "dirty" state, indicating modified data.
|
|
415
|
+
* @param isDirty - Boolean indicating whether the viewer has modified data
|
|
416
|
+
*/
|
|
417
|
+
setDirty(isDirty: boolean) {
|
|
418
|
+
this.sendMessage(ViewerMessageType.SET_DIRTY, isDirty);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Sends a close request to the parent, with information on whether the data is dirty.
|
|
423
|
+
* @param doc - Latest document data
|
|
424
|
+
* @param isDataDirty - Boolean indicating indicating modified data
|
|
425
|
+
*/
|
|
426
|
+
sendClose(doc: Document, isDataDirty: boolean): void {
|
|
427
|
+
this.sendMessage(ViewerMessageType.SEND_CLOSE, { doc, isDataDirty });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Sends an exception to parent.
|
|
432
|
+
* @param error - An error object with name and message or an error string
|
|
433
|
+
*/
|
|
434
|
+
sendException(error: { name?: string, message: string } | string): void {
|
|
435
|
+
this.sendMessage(ViewerMessageType.SEND_EXCEPTION, error);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
// Sets up the message listener for viewer to receive messages from the parent.
|
|
440
|
+
protected handleMessage(type: ClientMessageType, data: any): void {
|
|
441
|
+
|
|
442
|
+
console.debug("handleMessage called for:", type, "with data:", data);
|
|
443
|
+
|
|
444
|
+
if (!this.handler) {
|
|
445
|
+
if (type === ClientMessageType.INIT_ACK) {
|
|
446
|
+
console.warn("Handler not set. Storing INIT_ACK message for later processing.");
|
|
447
|
+
this.pendingInitAck = data; // Save INIT_ACK message
|
|
448
|
+
} else {
|
|
449
|
+
console.error("Message handler not found for type:", type);
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
switch (type) {
|
|
455
|
+
case ClientMessageType.INIT_ACK:
|
|
456
|
+
this.safeInvoke("onInitialData", this.handler, data);
|
|
457
|
+
break;
|
|
458
|
+
case ClientMessageType.DATA_CHANGE:
|
|
459
|
+
this.safeInvoke("onDataChange", this.handler, data);
|
|
460
|
+
break;
|
|
461
|
+
case ClientMessageType.REQUEST_CLOSE:
|
|
462
|
+
this.safeInvoke("onCloseRequest", this.handler);
|
|
463
|
+
break;
|
|
464
|
+
case ClientMessageType.REQUEST_SAVE:
|
|
465
|
+
this.safeInvoke("onSaveRequest", this.handler);
|
|
466
|
+
break;
|
|
467
|
+
case ClientMessageType.SAVE_SUCCESS:
|
|
468
|
+
this.handleOnSave(data);
|
|
469
|
+
break;
|
|
470
|
+
case ClientMessageType.SAVE_ERROR:
|
|
471
|
+
this.handleOnSave(data);
|
|
472
|
+
break;
|
|
473
|
+
default:
|
|
474
|
+
console.warn(`No handler found for message type: ${type}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Card SDK
|
|
481
|
+
|
|
482
|
+
// Enum defining message types sent from the card to the parent application
|
|
483
|
+
export enum CardMessageType {
|
|
484
|
+
APP_READY = "APP_READY", // Indicates the app is ready to be interacted with
|
|
485
|
+
INIT = "INIT" // Event to send the init key to the viewer
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export interface CardEventHandler {
|
|
489
|
+
onInit: (data: any) => void;
|
|
490
|
+
onError: (data: { error_code: string | number, message: string, data: any }) => void;
|
|
491
|
+
onRefreshRequest: (data: any) => void;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Use CardSdk to create webapp cards
|
|
496
|
+
*/
|
|
497
|
+
export class CardSdk extends DomeEmbeddedAppSdk {
|
|
498
|
+
private static instance: CardSdk; // Singleton instance of CardSdk
|
|
499
|
+
private handler: CardEventHandler | null = null; // Handler instance for client messages
|
|
500
|
+
private cryptoA01: any;
|
|
501
|
+
|
|
502
|
+
public dataStore: any;
|
|
503
|
+
|
|
504
|
+
private constructor() {
|
|
505
|
+
super();
|
|
506
|
+
this.cryptoA01 = new CryptoA01();
|
|
507
|
+
console.debug("CardSdk::constructor: done");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Static initialization method to get or create the singleton instance of CardSdk.
|
|
512
|
+
* @param secret - The card developer secret key
|
|
513
|
+
* @param handler - (Optional) Handler for different events emitted by the SDK
|
|
514
|
+
* @returns The singleton CardSdk instance
|
|
515
|
+
*/
|
|
516
|
+
public static async init(secret: string, handler?: CardEventHandler, options?: { devMode?: boolean }): Promise<CardSdk> {
|
|
517
|
+
try {
|
|
518
|
+
console.debug("CardSdk::init");
|
|
519
|
+
|
|
520
|
+
if (!CardSdk.instance) {
|
|
521
|
+
CardSdk.instance = new CardSdk();
|
|
522
|
+
// Initialize parent communication - REQUIRED!
|
|
523
|
+
CardSdk.instance.setupParentConnection()
|
|
524
|
+
.then(() => {
|
|
525
|
+
// Connection established with parents..
|
|
526
|
+
|
|
527
|
+
// Initialize SDK in async
|
|
528
|
+
CardSdk.instance.initializeCardSdk(secret);
|
|
529
|
+
})
|
|
530
|
+
.catch((err: any) => {
|
|
531
|
+
console.error(err);
|
|
532
|
+
})
|
|
533
|
+
} else {
|
|
534
|
+
return CardSdk.instance;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Setup handlers
|
|
538
|
+
if (handler) {
|
|
539
|
+
CardSdk.instance.setHandler(handler); // Set handler if provided during initialization
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return CardSdk.instance;
|
|
543
|
+
|
|
544
|
+
} catch (err) {
|
|
545
|
+
console.error("CardSdk: Unrecoverable error in init", err);
|
|
546
|
+
throw err;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Method to set or update the handler object.
|
|
552
|
+
* @param handler - Custom handler for different message types
|
|
553
|
+
*/
|
|
554
|
+
public setHandler(handler: CardEventHandler): void {
|
|
555
|
+
this.handler = handler;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Function to initialize SDK after instance is created
|
|
559
|
+
private async initializeCardSdk(secret: any) {
|
|
560
|
+
let TAG = "CardSdk::initializeCardSdk:";
|
|
561
|
+
try {
|
|
562
|
+
console.debug(TAG, "enter");
|
|
563
|
+
|
|
564
|
+
if (!secret) {
|
|
565
|
+
throw new Error("Invalid secret");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Get data from HTML
|
|
569
|
+
const data_af1 = (window as any).IT_DATA_AF1;
|
|
570
|
+
|
|
571
|
+
if (!data_af1) {
|
|
572
|
+
console.error(TAG, "No data");
|
|
573
|
+
throw new Error('No data');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const url = window.location.href;
|
|
577
|
+
console.debug(TAG, "url:", url);
|
|
578
|
+
const urlObject = new URL(url)
|
|
579
|
+
const segments = urlObject.pathname.split('/').filter(segment => segment);
|
|
580
|
+
let url_part;
|
|
581
|
+
if (segments.length > 1) {
|
|
582
|
+
url_part = segments[segments.length - 2];
|
|
583
|
+
} else if (segments.length == 1) {
|
|
584
|
+
url_part = segments[0];
|
|
585
|
+
} else {
|
|
586
|
+
throw new Error('Invalid URL');
|
|
587
|
+
}
|
|
588
|
+
const ss = url_part.split('').reverse().join('').substring(4, 25);
|
|
589
|
+
console.debug(TAG, "ss:", ss);
|
|
590
|
+
if (!ss) {
|
|
591
|
+
throw new Error('Cannot decrypt (1)');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const decData = await this.cryptoA01.decrypt(data_af1, secret, ss)
|
|
595
|
+
try {
|
|
596
|
+
const dataFromServer = JSON.parse(decData);
|
|
597
|
+
console.debug("CardSdk: dataFromServer:", dataFromServer);
|
|
598
|
+
if (!dataFromServer.ite) {
|
|
599
|
+
throw new Error("Invalid data");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
this.dataStore = {
|
|
603
|
+
'denc': dataFromServer.d,
|
|
604
|
+
'kw2': dataFromServer.kw2
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
CardSdk.instance.sendInit(dataFromServer.ite);
|
|
608
|
+
|
|
609
|
+
} catch (err) {
|
|
610
|
+
console.error("Initial Decryption failed (2):", err);
|
|
611
|
+
throw err;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
} catch (err: any) {
|
|
615
|
+
console.error(TAG, "Init failed:", err);
|
|
616
|
+
this.sendEventError('init_failed', err.message);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private async sendInit(token: string) {
|
|
621
|
+
this.sendMessage(CardMessageType.INIT, { 'token': token, sdk: { ver: pkg.version } });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Sets up the message listener for cards to receive messages from the parent.
|
|
625
|
+
protected handleMessage = (type: ClientMessageType, data: any) => {
|
|
626
|
+
|
|
627
|
+
if (!this.handler) {
|
|
628
|
+
throw new Error("Message handler not found!");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
switch (type) {
|
|
632
|
+
case ClientMessageType.INIT_ACK:
|
|
633
|
+
// Parent sent INIT_ACK
|
|
634
|
+
console.debug("CardSdk: INIT_ACK received");
|
|
635
|
+
|
|
636
|
+
this.dataStore.kw1 = data.key_wa1;
|
|
637
|
+
this.dataStore.iuid = data.iuid;
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
this.cryptoA01.decrypt(this.dataStore.denc, data.key_wa1 + this.dataStore.kw2, data.iuid)
|
|
641
|
+
.then((decData: any) => {
|
|
642
|
+
console.debug("CardSdk: INIT_ACK: decrypted data ", decData);
|
|
643
|
+
const decryptedData = JSON.parse(decData);
|
|
644
|
+
if (this.handler) this.safeInvoke("onInit", this.handler, { ...decryptedData, ui: data.ui });
|
|
645
|
+
// no need for orig enc data.. free to delete it
|
|
646
|
+
delete this.dataStore.denc;
|
|
647
|
+
})
|
|
648
|
+
.catch((err: any) => {
|
|
649
|
+
console.error("Final decrypt error", err);
|
|
650
|
+
throw err;
|
|
651
|
+
});
|
|
652
|
+
} catch (err: any) {
|
|
653
|
+
console.error("Decryption failed!", err);
|
|
654
|
+
this.sendEventError('dec2_failed', err.message);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
break;
|
|
658
|
+
|
|
659
|
+
case ClientMessageType.ERROR:
|
|
660
|
+
// Parent sent an ERROR
|
|
661
|
+
this.safeInvoke("onError", this.handler, data);
|
|
662
|
+
break;
|
|
663
|
+
|
|
664
|
+
case ClientMessageType.REFRESH:
|
|
665
|
+
// Asking for UI refresh
|
|
666
|
+
this.safeInvoke("onRefreshRequest", this.handler, data);
|
|
667
|
+
break;
|
|
668
|
+
|
|
669
|
+
default:
|
|
670
|
+
console.warn(`No handler found for message type: ${type}`);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Converts a JavaScript object to a JSON file.
|
|
676
|
+
*/
|
|
677
|
+
private async convertToJsonFile(object: any, type = 'json', fileName = 'file.json'): Promise<File> {
|
|
678
|
+
const jsonString = JSON.stringify(object, null, 2);
|
|
679
|
+
const jsonFileType = 'application/json';
|
|
680
|
+
const blob = new Blob([jsonString], { type: jsonFileType });
|
|
681
|
+
|
|
682
|
+
return new File([blob], fileName, { type: jsonFileType });
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Converts data to a file.
|
|
687
|
+
*/
|
|
688
|
+
private async convertToFile(data: any, fileName: string, fileType: 'json' | 'blob' = 'json', fileMimeType?: string): Promise<File> {
|
|
689
|
+
if (fileType === 'json') {
|
|
690
|
+
return this.convertToJsonFile(data, 'json', fileName);
|
|
691
|
+
} else {
|
|
692
|
+
const blob = new Blob([data], fileMimeType ? { type: fileMimeType } : undefined);
|
|
693
|
+
return new File([blob], fileName, { type: blob.type });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Get document associated with the current card
|
|
699
|
+
*/
|
|
700
|
+
public async getCardDocument(documentIuid?: string, cardIuid?: string): Promise<any> {
|
|
701
|
+
const fetchUrl = `/api/v1/${documentIuid}/`;
|
|
702
|
+
const createUrl = `/api/v1/documents/create_for/${cardIuid}`;
|
|
703
|
+
|
|
704
|
+
// Helper to handle response content
|
|
705
|
+
const parseResponse = async (response: Response) => {
|
|
706
|
+
const contentType = response.headers.get('Content-Type') || '';
|
|
707
|
+
if (contentType.includes('application/json')) return response.json();
|
|
708
|
+
if (contentType.includes('text/')) return response.text();
|
|
709
|
+
throw new Error(`Unsupported response type: ${contentType}`);
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
try {
|
|
713
|
+
// Try to fetch the document
|
|
714
|
+
const fetchResponse = await fetch(fetchUrl, { method: 'GET' });
|
|
715
|
+
if (fetchResponse.ok) return parseResponse(fetchResponse);
|
|
716
|
+
|
|
717
|
+
if (fetchResponse.status === 404) {
|
|
718
|
+
// Create the document if not found
|
|
719
|
+
const createResponse = await fetch(createUrl, { method: 'POST' });
|
|
720
|
+
if (!createResponse.ok) throw new Error(`Failed to create document: ${createResponse.status}`);
|
|
721
|
+
return parseResponse(createResponse);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
throw new Error(`Failed to fetch document: ${fetchResponse.status}`);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
console.error('Error handling document:', error);
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Uploads a file to the specified document IUID endpoint.
|
|
733
|
+
*/
|
|
734
|
+
public async saveDocument(fileObj: any, documentIuid: string): Promise<Response> {
|
|
735
|
+
// Convert the fileObj to a File
|
|
736
|
+
const { name, type } = fileObj;
|
|
737
|
+
|
|
738
|
+
const file = await this.convertToFile(fileObj, name, type);
|
|
739
|
+
|
|
740
|
+
// Construct the full URL
|
|
741
|
+
const apiUrl = `/api/v1/${documentIuid}/`;
|
|
742
|
+
|
|
743
|
+
// Create FormData and append the file
|
|
744
|
+
const formData = new FormData();
|
|
745
|
+
formData.append('file', file);
|
|
746
|
+
|
|
747
|
+
// Make the POST request
|
|
748
|
+
return fetch(apiUrl, {
|
|
749
|
+
method: 'PUT',
|
|
750
|
+
body: formData,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
// Send event on error (clients will get "onError" event)
|
|
756
|
+
private sendEventError(error_code: string, message: string, data: any = undefined) {
|
|
757
|
+
let data_to_send = {
|
|
758
|
+
'message': message,
|
|
759
|
+
'error_code': error_code,
|
|
760
|
+
'data': data
|
|
761
|
+
}
|
|
762
|
+
if (this.handler) this.safeInvoke("onError", this.handler, data_to_send);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
// Get username string from user object received from parent
|
|
767
|
+
public getUsername(userObj: any): string {
|
|
768
|
+
const nameObj = userObj?.name;
|
|
769
|
+
|
|
770
|
+
if (nameObj && Object.values(nameObj).some(value => value)) {
|
|
771
|
+
return getNameString(nameObj);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return '';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
}
|
|
778
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Class imports
|
|
2
|
+
export { ViewerSdk, CardSdk } from "./dome-sdk";
|
|
3
|
+
export { CryptoA01 } from "./crypto";
|
|
4
|
+
|
|
5
|
+
// Viewer Type imports
|
|
6
|
+
export type { ViewerMessageType, ViewerEventHandler, Document, UiProps } from "./dome-sdk";
|
|
7
|
+
|
|
8
|
+
// Card Type imports
|
|
9
|
+
export type { CardMessageType, CardEventHandler } from "./dome-sdk";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface AndroidBridgeType {
|
|
2
|
+
postMessage(message: any): void;
|
|
3
|
+
sendMessage(message: any): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface WebkitType {
|
|
7
|
+
messageHandlers: {
|
|
8
|
+
[handlerName: string]: {
|
|
9
|
+
postMessage(message: any): void;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Window {
|
|
15
|
+
AndroidBridge?: AndroidBridgeType;
|
|
16
|
+
webkit?: WebkitType;
|
|
17
|
+
app?: any;
|
|
18
|
+
receiveFromAndroid: (message: { type: string, data: any }) => void;
|
|
19
|
+
receiveFromIOS: (message: { type: string, data: any }) => void;
|
|
20
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper function to generate a unique requestId (UUID).
|
|
3
|
+
* This uses the browser's crypto API for random UUID generation.
|
|
4
|
+
*/
|
|
5
|
+
export function generateUUID(): string {
|
|
6
|
+
// If in a browser environment with crypto support (modern browsers)
|
|
7
|
+
if (typeof window !== 'undefined' && window.crypto && window.crypto.randomUUID) {
|
|
8
|
+
return window.crypto.randomUUID();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Fallback for non-browser environments (e.g., Node.js)
|
|
12
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
13
|
+
return crypto.randomUUID();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Fallback for environments without crypto support
|
|
17
|
+
throw new Error('UUID generation is not supported in this environment');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Helper function to get username from user object
|
|
22
|
+
* @param nameObj the user object
|
|
23
|
+
*/
|
|
24
|
+
export function getNameString(nameObj: any): string {
|
|
25
|
+
const { prefix = '', given = '', middle = '', family = '', suffix = '' } = nameObj || {};
|
|
26
|
+
const fullname = [prefix, given, middle, family, suffix].filter(namePart => namePart).join(' ');
|
|
27
|
+
|
|
28
|
+
return fullname.trim();
|
|
29
|
+
}
|
|
30
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"allowSyntheticDefaultImports": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"outDir": "./dist",
|
|
14
|
+
"rootDir": "./src",
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"lib": ["es2020", "dom"],
|
|
17
|
+
},
|
|
18
|
+
"ts-node": {
|
|
19
|
+
"compilerOptions": {
|
|
20
|
+
"module": "CommonJS"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"include": ["./src/**/*"],
|
|
24
|
+
"exclude": ["dist", "node_modules"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import webpack from 'webpack';
|
|
3
|
+
|
|
4
|
+
const config: webpack.Configuration = {
|
|
5
|
+
mode: "production",
|
|
6
|
+
entry: "./src/index.ts",
|
|
7
|
+
output: {
|
|
8
|
+
path: path.resolve(__dirname, "dist"),
|
|
9
|
+
clean: true,
|
|
10
|
+
filename: "index.js",
|
|
11
|
+
library: 'domeSdk',
|
|
12
|
+
libraryTarget: 'umd',
|
|
13
|
+
globalObject: "this",
|
|
14
|
+
},
|
|
15
|
+
resolve: {
|
|
16
|
+
extensions: [".ts", ".js"],
|
|
17
|
+
},
|
|
18
|
+
module: {
|
|
19
|
+
rules: [
|
|
20
|
+
{
|
|
21
|
+
test: /\.ts$/,
|
|
22
|
+
use: "ts-loader",
|
|
23
|
+
exclude: /node_modules/,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
optimization: {
|
|
28
|
+
splitChunks: {
|
|
29
|
+
chunks: "all",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
devtool: "source-map",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default config;
|