@learncard/partner-connect 0.3.10 → 0.4.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.
- package/README.md +184 -0
- package/dist/index.d.ts +209 -3
- package/dist/partner-connect.esm.js +822 -9
- package/dist/partner-connect.esm.js.map +1 -1
- package/dist/partner-connect.js +822 -8
- package/dist/partner-connect.js.map +1 -1
- package/dist/partner-connect.mjs +822 -9
- package/dist/partner-connect.mjs.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +274 -4
- package/src/mock-host.ts +887 -0
- package/src/mock-mode.test.ts +478 -0
- package/src/types.ts +158 -0
package/dist/partner-connect.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
var __defProp$
|
|
2
|
-
var __defNormalProp$
|
|
3
|
-
var __publicField$
|
|
1
|
+
var __defProp$2 = Object.defineProperty;
|
|
2
|
+
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
class PartnerConnectError extends Error {
|
|
5
5
|
constructor(code, message) {
|
|
6
6
|
super(message);
|
|
7
|
-
__publicField$
|
|
7
|
+
__publicField$2(this, "code");
|
|
8
8
|
this.name = "PartnerConnectError";
|
|
9
9
|
this.code = code;
|
|
10
10
|
Object.setPrototypeOf(this, PartnerConnectError.prototype);
|
|
@@ -31,10 +31,679 @@ class PartnerConnectError extends Error {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
var __defProp$1 = Object.defineProperty;
|
|
35
|
+
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
36
|
+
var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
37
|
+
const DEFAULT_DID = "did:web:mock.learncard.app:user";
|
|
38
|
+
const DEFAULT_NAMESPACE = "lc-mock";
|
|
39
|
+
const MOCK_PREFIX = "[LearnCard SDK \xB7 MOCK]";
|
|
40
|
+
const hasDocument = () => typeof document !== "undefined" && typeof document.createElement === "function";
|
|
41
|
+
const readAchievementName = (payload) => {
|
|
42
|
+
var _a, _b, _c, _d;
|
|
43
|
+
if (!payload || typeof payload !== "object") return "a credential";
|
|
44
|
+
const record = payload;
|
|
45
|
+
if (typeof record.templateAlias === "string" && record.templateAlias) {
|
|
46
|
+
const data = record.templateData;
|
|
47
|
+
if (data && typeof data === "object") {
|
|
48
|
+
const fields = data;
|
|
49
|
+
const named = (_c = (_b = (_a = fields.name) != null ? _a : fields.achievementName) != null ? _b : fields.courseName) != null ? _c : fields.title;
|
|
50
|
+
if (typeof named === "string" && named) return named;
|
|
51
|
+
}
|
|
52
|
+
return record.templateAlias;
|
|
53
|
+
}
|
|
54
|
+
const credential = (_d = record.credential) != null ? _d : record;
|
|
55
|
+
if (credential && typeof credential === "object") {
|
|
56
|
+
const subject = credential.credentialSubject;
|
|
57
|
+
const achievement = subject == null ? void 0 : subject.achievement;
|
|
58
|
+
if (achievement && typeof achievement.name === "string" && achievement.name) {
|
|
59
|
+
return achievement.name;
|
|
60
|
+
}
|
|
61
|
+
if (typeof credential.name === "string" && credential.name) return credential.name;
|
|
62
|
+
}
|
|
63
|
+
return "a credential";
|
|
64
|
+
};
|
|
65
|
+
class MockHost {
|
|
66
|
+
constructor(options) {
|
|
67
|
+
__publicField$1(this, "options");
|
|
68
|
+
/** In-memory counter fallback used when persistence is off/unavailable. */
|
|
69
|
+
__publicField$1(this, "memoryCounters", /* @__PURE__ */ new Map());
|
|
70
|
+
/** Session-scoped credential store; reads reflect writes and seeds. */
|
|
71
|
+
__publicField$1(this, "credentials", []);
|
|
72
|
+
__publicField$1(this, "identity");
|
|
73
|
+
/** DOM nodes injected for visual feedback, tracked for cleanup. */
|
|
74
|
+
__publicField$1(this, "domNodes", /* @__PURE__ */ new Set());
|
|
75
|
+
/** The app's single AI Topic, created lazily by the first AI session. */
|
|
76
|
+
__publicField$1(this, "aiTopic", null);
|
|
77
|
+
__publicField$1(this, "styleEl", null);
|
|
78
|
+
__publicField$1(this, "stackEl", null);
|
|
79
|
+
__publicField$1(this, "activeToasts", /* @__PURE__ */ new Map());
|
|
80
|
+
/** Pending exit-animation timers, tracked so `destroy()` can cancel them. */
|
|
81
|
+
__publicField$1(this, "exitTimers", /* @__PURE__ */ new Set());
|
|
82
|
+
__publicField$1(this, "idSeq", 0);
|
|
83
|
+
__publicField$1(this, "destroyed", false);
|
|
84
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
85
|
+
this.options = {
|
|
86
|
+
ui: (_a = options == null ? void 0 : options.ui) != null ? _a : true,
|
|
87
|
+
log: (_b = options == null ? void 0 : options.log) != null ? _b : true,
|
|
88
|
+
persist: (_c = options == null ? void 0 : options.persist) != null ? _c : true,
|
|
89
|
+
namespace: (options == null ? void 0 : options.namespace) || DEFAULT_NAMESPACE
|
|
90
|
+
};
|
|
91
|
+
this.identity = {
|
|
92
|
+
...(_d = options == null ? void 0 : options.identity) != null ? _d : {},
|
|
93
|
+
did: ((_e = options == null ? void 0 : options.identity) == null ? void 0 : _e.did) || (options == null ? void 0 : options.did) || DEFAULT_DID
|
|
94
|
+
};
|
|
95
|
+
for (const seed of (_f = options == null ? void 0 : options.credentials) != null ? _f : []) {
|
|
96
|
+
this.addCredential({
|
|
97
|
+
name: seed.name || seed.templateAlias || "Mock Credential",
|
|
98
|
+
templateAlias: seed.templateAlias,
|
|
99
|
+
boostUri: seed.boostUri,
|
|
100
|
+
recipient: seed.recipient,
|
|
101
|
+
status: seed.status
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
for (const [key, value] of Object.entries((_g = options == null ? void 0 : options.counters) != null ? _g : {})) {
|
|
105
|
+
if (this.readCounter(key) === void 0) this.writeCounter(key, value);
|
|
106
|
+
}
|
|
107
|
+
this.announce();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a simulated response for one SDK request. Mirrors the real host:
|
|
111
|
+
* direct actions map 1:1, and `APP_EVENT` is dispatched by its `type`.
|
|
112
|
+
*/
|
|
113
|
+
handle(action, payload) {
|
|
114
|
+
var _a, _b;
|
|
115
|
+
if (this.destroyed) {
|
|
116
|
+
return Promise.reject({
|
|
117
|
+
code: "SDK_DESTROYED",
|
|
118
|
+
message: "Mock host was destroyed before the request completed"
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
this.log(action, payload);
|
|
122
|
+
if (action === "APP_EVENT") {
|
|
123
|
+
return this.handleAppEvent(payload);
|
|
124
|
+
}
|
|
125
|
+
switch (action) {
|
|
126
|
+
case "REQUEST_IDENTITY":
|
|
127
|
+
this.toast({
|
|
128
|
+
icon: "\u{1F464}",
|
|
129
|
+
segments: ["In LearnCard, the user would sign in. Returning a mock identity."]
|
|
130
|
+
});
|
|
131
|
+
return Promise.resolve({
|
|
132
|
+
token: `mock-token-${Date.now()}`,
|
|
133
|
+
user: { ...this.identity }
|
|
134
|
+
});
|
|
135
|
+
case "SEND_CREDENTIAL": {
|
|
136
|
+
const name = readAchievementName(payload);
|
|
137
|
+
const credentialId = `mock-credential-${Date.now()}-${this.idSeq += 1}`;
|
|
138
|
+
this.addCredential({
|
|
139
|
+
name,
|
|
140
|
+
credentialUri: credentialId,
|
|
141
|
+
credential: payload == null ? void 0 : payload.credential
|
|
142
|
+
});
|
|
143
|
+
this.showClaimToast(name);
|
|
144
|
+
return Promise.resolve({ credentialId, stored: true });
|
|
145
|
+
}
|
|
146
|
+
case "REQUEST_CONSENT": {
|
|
147
|
+
const redirect = Boolean(payload == null ? void 0 : payload.redirect);
|
|
148
|
+
this.showConsentBanner(redirect);
|
|
149
|
+
return Promise.resolve({ granted: true });
|
|
150
|
+
}
|
|
151
|
+
case "LAUNCH_FEATURE": {
|
|
152
|
+
const featurePath = (_a = payload == null ? void 0 : payload.featurePath) != null ? _a : "";
|
|
153
|
+
this.toast({
|
|
154
|
+
icon: "\u{1F680}",
|
|
155
|
+
segments: featurePath ? ["In LearnCard, this would open ", { b: featurePath }, "."] : ["In LearnCard, this would open a feature screen."]
|
|
156
|
+
});
|
|
157
|
+
return Promise.resolve({ launched: true, featurePath });
|
|
158
|
+
}
|
|
159
|
+
case "ASK_CREDENTIAL_SEARCH": {
|
|
160
|
+
const held = this.selfCredentials();
|
|
161
|
+
this.toast({
|
|
162
|
+
icon: "\u{1F50D}",
|
|
163
|
+
segments: held.length ? ["The user could share ", { b: String(held.length) }, " credential(s)."] : [
|
|
164
|
+
"In LearnCard, the user would be asked to share matching credentials. None in mock."
|
|
165
|
+
]
|
|
166
|
+
});
|
|
167
|
+
return Promise.resolve({
|
|
168
|
+
verifiablePresentation: {
|
|
169
|
+
verifiableCredential: held.map((c) => c.credential)
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
case "ASK_CREDENTIAL_SPECIFIC": {
|
|
174
|
+
const credentialId = payload == null ? void 0 : payload.credentialId;
|
|
175
|
+
const found = this.credentials.find((c) => c.credentialUri === credentialId);
|
|
176
|
+
this.toast({
|
|
177
|
+
icon: "\u{1F50D}",
|
|
178
|
+
segments: found ? ["Sharing ", { b: found.name }, "."] : [
|
|
179
|
+
"In LearnCard, the user would be asked to share a credential. Not found in mock."
|
|
180
|
+
]
|
|
181
|
+
});
|
|
182
|
+
return Promise.resolve({ credential: found == null ? void 0 : found.credential });
|
|
183
|
+
}
|
|
184
|
+
case "INITIATE_TEMPLATE_ISSUE": {
|
|
185
|
+
const input = payload;
|
|
186
|
+
const templateId = (_b = input == null ? void 0 : input.templateId) != null ? _b : "";
|
|
187
|
+
const recipients = Array.isArray(input == null ? void 0 : input.draftRecipients) ? input == null ? void 0 : input.draftRecipients : [];
|
|
188
|
+
for (const recipient of recipients) {
|
|
189
|
+
this.addCredential({
|
|
190
|
+
name: templateId || "Boost",
|
|
191
|
+
boostUri: templateId,
|
|
192
|
+
recipient,
|
|
193
|
+
status: "pending"
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
this.toast({
|
|
197
|
+
icon: "\u{1F4E4}",
|
|
198
|
+
segments: recipients.length ? [
|
|
199
|
+
"This would issue to ",
|
|
200
|
+
{ b: String(recipients.length) },
|
|
201
|
+
" recipient(s)."
|
|
202
|
+
] : ["In LearnCard, this would open the Send Boost flow."]
|
|
203
|
+
});
|
|
204
|
+
return Promise.resolve({ issued: true });
|
|
205
|
+
}
|
|
206
|
+
case "REQUEST_LEARNER_CONTEXT": {
|
|
207
|
+
const opts = payload != null ? payload : {};
|
|
208
|
+
const includeCredentials = opts.includeCredentials !== false;
|
|
209
|
+
const structured = opts.format === "structured";
|
|
210
|
+
const held = includeCredentials ? this.selfCredentials() : [];
|
|
211
|
+
this.toast({
|
|
212
|
+
icon: "\u{1F9E0}",
|
|
213
|
+
segments: !includeCredentials ? ["Learner profile requested with credentials excluded."] : held.length ? ["Learner profile: ", { b: String(held.length) }, " credential(s)."] : ["In LearnCard, the user's learner profile would load. Empty in mock."]
|
|
214
|
+
});
|
|
215
|
+
const prompt = !includeCredentials ? "Mock learner context: credentials were not requested." : held.length ? `Mock learner context. Credentials held: ${held.map((c) => c.name).join(", ")}.` : "Mock learner context: this user has no credentials in standalone mode.";
|
|
216
|
+
return Promise.resolve({
|
|
217
|
+
status: "ready",
|
|
218
|
+
prompt,
|
|
219
|
+
did: this.identity.did,
|
|
220
|
+
// Matches the real host: `raw` only ships for format: 'structured'.
|
|
221
|
+
...structured ? { raw: { credentials: held.map((c) => c.credential) } } : {}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
case "GET_SYNC_STATUS":
|
|
225
|
+
this.toast({
|
|
226
|
+
icon: "\u{1F504}",
|
|
227
|
+
segments: [
|
|
228
|
+
"In LearnCard, this reports data sync progress. Mock reports ready."
|
|
229
|
+
]
|
|
230
|
+
});
|
|
231
|
+
return Promise.resolve({
|
|
232
|
+
status: "ready",
|
|
233
|
+
progress: {
|
|
234
|
+
totalCredentials: 0,
|
|
235
|
+
completedCredentials: 0,
|
|
236
|
+
failedCredentials: 0,
|
|
237
|
+
retryCount: 0
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
default:
|
|
241
|
+
this.toast({
|
|
242
|
+
icon: "\u2728",
|
|
243
|
+
segments: ["In LearnCard, this would run ", { b: action }, "."]
|
|
244
|
+
});
|
|
245
|
+
return Promise.resolve({});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Tear down injected UI and clear in-memory state. */
|
|
249
|
+
destroy() {
|
|
250
|
+
this.destroyed = true;
|
|
251
|
+
for (const entry of this.activeToasts.values()) clearTimeout(entry.timeoutId);
|
|
252
|
+
this.activeToasts.clear();
|
|
253
|
+
for (const timer of this.exitTimers) clearTimeout(timer);
|
|
254
|
+
this.exitTimers.clear();
|
|
255
|
+
for (const node of this.domNodes) node.remove();
|
|
256
|
+
this.domNodes.clear();
|
|
257
|
+
if (this.stackEl) {
|
|
258
|
+
this.stackEl.remove();
|
|
259
|
+
this.stackEl = null;
|
|
260
|
+
}
|
|
261
|
+
if (this.styleEl) {
|
|
262
|
+
this.styleEl.remove();
|
|
263
|
+
this.styleEl = null;
|
|
264
|
+
}
|
|
265
|
+
this.memoryCounters.clear();
|
|
266
|
+
this.credentials.length = 0;
|
|
267
|
+
}
|
|
268
|
+
handleAppEvent(event) {
|
|
269
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
270
|
+
const type = typeof (event == null ? void 0 : event.type) === "string" ? event.type : "";
|
|
271
|
+
switch (type) {
|
|
272
|
+
case "send-credential": {
|
|
273
|
+
const name = readAchievementName(event);
|
|
274
|
+
const templateAlias = typeof event.templateAlias === "string" ? event.templateAlias : void 0;
|
|
275
|
+
const boostUri = `lc:mock:boost:${String((_a = event.templateAlias) != null ? _a : "template")}`;
|
|
276
|
+
if (event.preventDuplicateClaim) {
|
|
277
|
+
const existing = this.selfCredentials().find(
|
|
278
|
+
(c) => this.matchesTemplate(c, { templateAlias, boostUri })
|
|
279
|
+
);
|
|
280
|
+
if (existing) {
|
|
281
|
+
this.toast({
|
|
282
|
+
icon: "\u2705",
|
|
283
|
+
segments: ["The user already has ", { b: existing.name }, "."]
|
|
284
|
+
});
|
|
285
|
+
return Promise.resolve({
|
|
286
|
+
credentialUri: existing.credentialUri,
|
|
287
|
+
boostUri: (_b = existing.boostUri) != null ? _b : boostUri,
|
|
288
|
+
alreadyClaimed: true,
|
|
289
|
+
hasCredential: true,
|
|
290
|
+
status: existing.status,
|
|
291
|
+
receivedDate: existing.receivedDate
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const record = this.addCredential({
|
|
296
|
+
name,
|
|
297
|
+
templateAlias,
|
|
298
|
+
boostUri,
|
|
299
|
+
status: "claimed"
|
|
300
|
+
});
|
|
301
|
+
this.showClaimToast(name);
|
|
302
|
+
return Promise.resolve({
|
|
303
|
+
credentialUri: record.credentialUri,
|
|
304
|
+
boostUri: (_c = record.boostUri) != null ? _c : boostUri,
|
|
305
|
+
alreadyClaimed: false,
|
|
306
|
+
hasCredential: true,
|
|
307
|
+
status: "claimed",
|
|
308
|
+
receivedDate: record.receivedDate
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
case "check-credential": {
|
|
312
|
+
const held = this.selfCredentials().find((c) => this.matchesTemplate(c, event));
|
|
313
|
+
this.toast({
|
|
314
|
+
icon: "\u{1F50E}",
|
|
315
|
+
segments: held ? ["The user already has ", { b: held.name }, "."] : ["Mock: the user doesn't have this credential yet."]
|
|
316
|
+
});
|
|
317
|
+
return Promise.resolve(
|
|
318
|
+
held ? {
|
|
319
|
+
hasCredential: true,
|
|
320
|
+
credentialUri: held.credentialUri,
|
|
321
|
+
receivedDate: held.receivedDate,
|
|
322
|
+
status: held.status
|
|
323
|
+
} : { hasCredential: false }
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
case "check-issuance-status": {
|
|
327
|
+
const recipient = typeof event.recipient === "string" ? event.recipient : "";
|
|
328
|
+
const match = this.credentials.find(
|
|
329
|
+
(c) => this.matchesTemplate(c, event) && c.recipient === recipient
|
|
330
|
+
);
|
|
331
|
+
this.toast({
|
|
332
|
+
icon: "\u{1F50E}",
|
|
333
|
+
segments: match ? ["Issued to ", { b: recipient }, " \u2014 ", { b: match.status }, "."] : ["Mock: not sent to this recipient yet."]
|
|
334
|
+
});
|
|
335
|
+
return Promise.resolve(
|
|
336
|
+
match ? {
|
|
337
|
+
sent: true,
|
|
338
|
+
credentialUri: match.credentialUri,
|
|
339
|
+
sentDate: match.sentDate,
|
|
340
|
+
claimedDate: match.claimedDate,
|
|
341
|
+
status: match.status
|
|
342
|
+
} : { sent: false }
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
case "get-template-recipients": {
|
|
346
|
+
const matched = this.credentials.filter((c) => this.matchesTemplate(c, event));
|
|
347
|
+
const limit = typeof event.limit === "number" && event.limit > 0 ? event.limit : void 0;
|
|
348
|
+
const parsedCursor = typeof event.cursor === "string" ? Number.parseInt(event.cursor, 10) : 0;
|
|
349
|
+
const offset = Number.isFinite(parsedCursor) && parsedCursor > 0 ? parsedCursor : 0;
|
|
350
|
+
const page = limit ? matched.slice(offset, offset + limit) : matched.slice(offset);
|
|
351
|
+
const nextOffset = offset + page.length;
|
|
352
|
+
const hasMore = nextOffset < matched.length;
|
|
353
|
+
this.toast({
|
|
354
|
+
icon: "\u{1F465}",
|
|
355
|
+
segments: [
|
|
356
|
+
"In LearnCard, this lists recipients. ",
|
|
357
|
+
{ b: String(matched.length) },
|
|
358
|
+
" in mock."
|
|
359
|
+
]
|
|
360
|
+
});
|
|
361
|
+
return Promise.resolve({
|
|
362
|
+
records: page.map((c) => this.toRecipientRecord(c)),
|
|
363
|
+
hasMore,
|
|
364
|
+
...hasMore ? { cursor: String(nextOffset) } : {},
|
|
365
|
+
total: matched.length
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
case "send-notification": {
|
|
369
|
+
const title = typeof event.title === "string" ? event.title : "";
|
|
370
|
+
const body = typeof event.body === "string" ? event.body : "";
|
|
371
|
+
const text = [title, body].filter(Boolean).join(" \u2014 ");
|
|
372
|
+
this.toast({
|
|
373
|
+
icon: "\u{1F514}",
|
|
374
|
+
segments: text ? ["The user would be notified: ", { b: text }] : ["In LearnCard, the user would receive a notification."]
|
|
375
|
+
});
|
|
376
|
+
return Promise.resolve({ sent: true });
|
|
377
|
+
}
|
|
378
|
+
case "increment-counter": {
|
|
379
|
+
const key = String((_d = event.key) != null ? _d : "");
|
|
380
|
+
const amount = typeof event.amount === "number" ? event.amount : 0;
|
|
381
|
+
const previous = (_f = (_e = this.readCounter(key)) == null ? void 0 : _e.value) != null ? _f : 0;
|
|
382
|
+
const next = previous + amount;
|
|
383
|
+
this.writeCounter(key, next);
|
|
384
|
+
this.toast({
|
|
385
|
+
icon: "\u{1F522}",
|
|
386
|
+
segments: ["Counter ", { b: key }, " \u2192 ", { b: String(next) }, "."]
|
|
387
|
+
});
|
|
388
|
+
return Promise.resolve({ key, previousValue: previous, newValue: next });
|
|
389
|
+
}
|
|
390
|
+
case "get-counter": {
|
|
391
|
+
const key = String((_g = event.key) != null ? _g : "");
|
|
392
|
+
const stored = this.readCounter(key);
|
|
393
|
+
const value = (_h = stored == null ? void 0 : stored.value) != null ? _h : 0;
|
|
394
|
+
this.toast({
|
|
395
|
+
icon: "\u{1F522}",
|
|
396
|
+
segments: ["Counter ", { b: key }, " is ", { b: String(value) }, "."]
|
|
397
|
+
});
|
|
398
|
+
return Promise.resolve({
|
|
399
|
+
key,
|
|
400
|
+
value,
|
|
401
|
+
updatedAt: (_i = stored == null ? void 0 : stored.updatedAt) != null ? _i : null
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
case "get-counters": {
|
|
405
|
+
const requested = Array.isArray(event.keys) ? event.keys.map(String) : this.allCounterKeys();
|
|
406
|
+
const counters = requested.map((key) => {
|
|
407
|
+
var _a2, _b2;
|
|
408
|
+
const stored = this.readCounter(key);
|
|
409
|
+
return { key, value: (_a2 = stored == null ? void 0 : stored.value) != null ? _a2 : 0, updatedAt: (_b2 = stored == null ? void 0 : stored.updatedAt) != null ? _b2 : null };
|
|
410
|
+
});
|
|
411
|
+
this.toast({
|
|
412
|
+
icon: "\u{1F522}",
|
|
413
|
+
segments: ["Read ", { b: String(counters.length) }, " counter(s)."]
|
|
414
|
+
});
|
|
415
|
+
return Promise.resolve({ counters });
|
|
416
|
+
}
|
|
417
|
+
case "send-ai-session-credential": {
|
|
418
|
+
const sessionTitle = typeof event.sessionTitle === "string" && event.sessionTitle ? event.sessionTitle : "AI session";
|
|
419
|
+
const sessionBoostUri = this.nextUri("lc:mock:session-boost");
|
|
420
|
+
const record = this.addCredential({
|
|
421
|
+
name: sessionTitle,
|
|
422
|
+
boostUri: sessionBoostUri,
|
|
423
|
+
status: "claimed"
|
|
424
|
+
});
|
|
425
|
+
this.toast({
|
|
426
|
+
icon: "\u2705",
|
|
427
|
+
segments: [
|
|
428
|
+
"In LearnCard, an AI session credential would be saved: ",
|
|
429
|
+
{ b: sessionTitle }
|
|
430
|
+
]
|
|
431
|
+
});
|
|
432
|
+
const isNewTopic = this.aiTopic === null;
|
|
433
|
+
if (!this.aiTopic) {
|
|
434
|
+
this.aiTopic = {
|
|
435
|
+
topicUri: this.nextUri("lc:mock:topic"),
|
|
436
|
+
topicCredentialUri: this.nextUri("lc:mock:topic-credential")
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return Promise.resolve({
|
|
440
|
+
topicUri: this.aiTopic.topicUri,
|
|
441
|
+
...isNewTopic ? { topicCredentialUri: this.aiTopic.topicCredentialUri } : {},
|
|
442
|
+
sessionCredentialUri: record.credentialUri,
|
|
443
|
+
sessionBoostUri,
|
|
444
|
+
isNewTopic
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
default:
|
|
448
|
+
this.toast({
|
|
449
|
+
icon: "\u2728",
|
|
450
|
+
segments: ["In LearnCard, this would run ", { b: type }, "."]
|
|
451
|
+
});
|
|
452
|
+
return Promise.resolve({});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
nextUri(prefix) {
|
|
456
|
+
this.idSeq += 1;
|
|
457
|
+
return `${prefix}:${Date.now()}-${this.idSeq}`;
|
|
458
|
+
}
|
|
459
|
+
buildMockVc(name, id) {
|
|
460
|
+
return {
|
|
461
|
+
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
|
462
|
+
id,
|
|
463
|
+
type: ["VerifiableCredential"],
|
|
464
|
+
issuer: this.identity.did,
|
|
465
|
+
credentialSubject: { id: this.identity.did, achievement: { name } },
|
|
466
|
+
_mock: true
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
addCredential(input) {
|
|
470
|
+
var _a, _b, _c;
|
|
471
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
472
|
+
const status = (_a = input.status) != null ? _a : "claimed";
|
|
473
|
+
const credentialUri = (_b = input.credentialUri) != null ? _b : this.nextUri("lc:mock:credential");
|
|
474
|
+
const record = {
|
|
475
|
+
credentialUri,
|
|
476
|
+
boostUri: input.boostUri,
|
|
477
|
+
templateAlias: input.templateAlias,
|
|
478
|
+
name: input.name,
|
|
479
|
+
recipient: input.recipient || this.identity.did,
|
|
480
|
+
status,
|
|
481
|
+
sentDate: now,
|
|
482
|
+
claimedDate: status === "claimed" ? now : void 0,
|
|
483
|
+
receivedDate: status === "claimed" ? now : void 0,
|
|
484
|
+
credential: (_c = input.credential) != null ? _c : this.buildMockVc(input.name, credentialUri)
|
|
485
|
+
};
|
|
486
|
+
this.credentials.push(record);
|
|
487
|
+
return record;
|
|
488
|
+
}
|
|
489
|
+
selfCredentials() {
|
|
490
|
+
return this.credentials.filter((c) => c.recipient === this.identity.did);
|
|
491
|
+
}
|
|
492
|
+
matchesTemplate(c, q) {
|
|
493
|
+
if (typeof q.templateAlias === "string") return c.templateAlias === q.templateAlias;
|
|
494
|
+
if (typeof q.boostUri === "string") return c.boostUri === q.boostUri;
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
toRecipientRecord(c) {
|
|
498
|
+
return {
|
|
499
|
+
recipientProfileId: c.recipient,
|
|
500
|
+
recipientDisplayName: c.recipient,
|
|
501
|
+
sentDate: c.sentDate,
|
|
502
|
+
claimedDate: c.claimedDate,
|
|
503
|
+
credentialUri: c.credentialUri,
|
|
504
|
+
status: c.status
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
counterStorageKey() {
|
|
508
|
+
return `${this.options.namespace}:counters`;
|
|
509
|
+
}
|
|
510
|
+
loadCounters() {
|
|
511
|
+
if (this.options.persist && typeof localStorage !== "undefined") {
|
|
512
|
+
try {
|
|
513
|
+
const raw = localStorage.getItem(this.counterStorageKey());
|
|
514
|
+
if (raw) return JSON.parse(raw);
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const fromMemory = {};
|
|
519
|
+
for (const [key, value] of this.memoryCounters) fromMemory[key] = value;
|
|
520
|
+
return fromMemory;
|
|
521
|
+
}
|
|
522
|
+
readCounter(key) {
|
|
523
|
+
return this.loadCounters()[key];
|
|
524
|
+
}
|
|
525
|
+
allCounterKeys() {
|
|
526
|
+
return Object.keys(this.loadCounters());
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* The load → patch → save cycle below runs synchronously within one task,
|
|
530
|
+
* so increments in the same tab can never interleave. Concurrent writes
|
|
531
|
+
* from *other tabs* sharing the namespace can still be lost — acceptable
|
|
532
|
+
* for a dev-only mock, and inherent to `localStorage` without web locks.
|
|
533
|
+
*/
|
|
534
|
+
writeCounter(key, value) {
|
|
535
|
+
const entry = { value, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
536
|
+
if (this.options.persist && typeof localStorage !== "undefined") {
|
|
537
|
+
try {
|
|
538
|
+
const all = this.loadCounters();
|
|
539
|
+
all[key] = entry;
|
|
540
|
+
localStorage.setItem(this.counterStorageKey(), JSON.stringify(all));
|
|
541
|
+
return;
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
this.memoryCounters.set(key, entry);
|
|
546
|
+
}
|
|
547
|
+
log(action, payload) {
|
|
548
|
+
if (!this.options.log) return;
|
|
549
|
+
console.log(`${MOCK_PREFIX} ${action}`, payload != null ? payload : "");
|
|
550
|
+
}
|
|
551
|
+
announce() {
|
|
552
|
+
if (!this.options.log) return;
|
|
553
|
+
console.log(
|
|
554
|
+
`${MOCK_PREFIX} Standalone mock mode is active. The SDK is simulating the LearnCard host locally. When embedded in a real host, these calls run against it unchanged.`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
showClaimToast(credentialName) {
|
|
558
|
+
this.toast({
|
|
559
|
+
icon: "\u2705",
|
|
560
|
+
ttl: 5200,
|
|
561
|
+
segments: ["In LearnCard, the user would receive ", { b: credentialName }, " here."]
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
showConsentBanner(redirectIgnored = false) {
|
|
565
|
+
this.toast({
|
|
566
|
+
icon: "\u{1F513}",
|
|
567
|
+
tone: "positive",
|
|
568
|
+
segments: redirectIgnored ? ["Consent auto-granted. Redirect ignored in mock."] : ["The user would review and grant consent. Auto-granted in mock."]
|
|
569
|
+
});
|
|
570
|
+
if (redirectIgnored) {
|
|
571
|
+
this.note(
|
|
572
|
+
"requestConsent: 'redirect' is ignored in mock mode \u2014 the real host would navigate to the contract's redirectUrl with the VP in the URL."
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
note(message) {
|
|
577
|
+
if (!this.options.log) return;
|
|
578
|
+
console.log(`${MOCK_PREFIX} ${message}`);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Show a branded toast describing what the real host would do. Identical
|
|
582
|
+
* messages coalesce into one toast with a ×N counter so repeated or polled
|
|
583
|
+
* calls never spam the screen.
|
|
584
|
+
*/
|
|
585
|
+
toast(spec) {
|
|
586
|
+
var _a, _b;
|
|
587
|
+
if (!this.options.ui || !hasDocument()) return;
|
|
588
|
+
const tone = (_a = spec.tone) != null ? _a : "default";
|
|
589
|
+
const ttl = (_b = spec.ttl) != null ? _b : 4200;
|
|
590
|
+
const text = spec.segments.map((s) => typeof s === "string" ? s : s.b).join("");
|
|
591
|
+
const key = `${tone}|${spec.icon}|${text}`;
|
|
592
|
+
const existing = this.activeToasts.get(key);
|
|
593
|
+
if (existing) {
|
|
594
|
+
existing.count += 1;
|
|
595
|
+
existing.countEl.textContent = `\xD7${existing.count}`;
|
|
596
|
+
existing.countEl.style.display = "";
|
|
597
|
+
clearTimeout(existing.timeoutId);
|
|
598
|
+
existing.timeoutId = setTimeout(() => this.dismissToast(key), ttl);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const stack = this.ensureStack();
|
|
602
|
+
if (!stack) return;
|
|
603
|
+
this.ensureStyles();
|
|
604
|
+
const toast = document.createElement("div");
|
|
605
|
+
toast.className = `lc-mock-toast lc-mock-toast--${tone}`;
|
|
606
|
+
const badge = document.createElement("div");
|
|
607
|
+
badge.className = "lc-mock-badge";
|
|
608
|
+
const name = document.createElement("span");
|
|
609
|
+
name.className = "lc-mock-badge-name";
|
|
610
|
+
name.textContent = `${spec.icon} LearnCard`;
|
|
611
|
+
const pill = document.createElement("span");
|
|
612
|
+
pill.className = "lc-mock-pill";
|
|
613
|
+
pill.textContent = "MOCK";
|
|
614
|
+
badge.append(name, pill);
|
|
615
|
+
const body = document.createElement("div");
|
|
616
|
+
body.className = "lc-mock-body";
|
|
617
|
+
for (const seg of spec.segments) {
|
|
618
|
+
if (typeof seg === "string") {
|
|
619
|
+
body.append(seg);
|
|
620
|
+
} else {
|
|
621
|
+
const strong = document.createElement("strong");
|
|
622
|
+
strong.textContent = seg.b;
|
|
623
|
+
body.appendChild(strong);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const countEl = document.createElement("span");
|
|
627
|
+
countEl.className = "lc-mock-count";
|
|
628
|
+
countEl.style.display = "none";
|
|
629
|
+
body.appendChild(countEl);
|
|
630
|
+
toast.append(badge, body);
|
|
631
|
+
stack.appendChild(toast);
|
|
632
|
+
this.domNodes.add(toast);
|
|
633
|
+
const timeoutId = setTimeout(() => this.dismissToast(key), ttl);
|
|
634
|
+
this.activeToasts.set(key, { node: toast, timeoutId, count: 1, countEl });
|
|
635
|
+
}
|
|
636
|
+
dismissToast(key) {
|
|
637
|
+
const entry = this.activeToasts.get(key);
|
|
638
|
+
if (!entry) return;
|
|
639
|
+
this.activeToasts.delete(key);
|
|
640
|
+
clearTimeout(entry.timeoutId);
|
|
641
|
+
const { node } = entry;
|
|
642
|
+
node.classList.add("lc-mock-out");
|
|
643
|
+
const exitTimer = setTimeout(() => {
|
|
644
|
+
this.exitTimers.delete(exitTimer);
|
|
645
|
+
node.remove();
|
|
646
|
+
this.domNodes.delete(node);
|
|
647
|
+
}, 200);
|
|
648
|
+
this.exitTimers.add(exitTimer);
|
|
649
|
+
}
|
|
650
|
+
ensureStack() {
|
|
651
|
+
if (!document.body) return null;
|
|
652
|
+
if (this.stackEl && document.body.contains(this.stackEl)) return this.stackEl;
|
|
653
|
+
const stack = document.createElement("div");
|
|
654
|
+
stack.className = "lc-mock-stack";
|
|
655
|
+
document.body.appendChild(stack);
|
|
656
|
+
this.stackEl = stack;
|
|
657
|
+
return stack;
|
|
658
|
+
}
|
|
659
|
+
ensureStyles() {
|
|
660
|
+
if (!this.options.ui || !hasDocument() || this.styleEl || !document.head) return;
|
|
661
|
+
const style = document.createElement("style");
|
|
662
|
+
style.textContent = `
|
|
663
|
+
@keyframes lc-mock-in { from { opacity: 0; transform: translateY(10px) scale(0.98); } to { opacity: 1; transform: none; } }
|
|
664
|
+
@keyframes lc-mock-out { to { opacity: 0; transform: translateY(6px); } }
|
|
665
|
+
.lc-mock-stack {
|
|
666
|
+
position: fixed; bottom: 20px; right: 20px; z-index: 2147483647;
|
|
667
|
+
display: flex; flex-direction: column; gap: 10px; align-items: flex-end;
|
|
668
|
+
pointer-events: none; max-width: min(360px, calc(100vw - 40px));
|
|
669
|
+
}
|
|
670
|
+
.lc-mock-toast {
|
|
671
|
+
pointer-events: auto; width: 100%; box-sizing: border-box; padding: 11px 14px; border-radius: 14px;
|
|
672
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13.5px;
|
|
673
|
+
line-height: 1.45; box-shadow: 0 10px 30px rgba(24,34,78,0.22); animation: lc-mock-in 180ms cubic-bezier(0.2,0.8,0.2,1);
|
|
674
|
+
}
|
|
675
|
+
.lc-mock-toast.lc-mock-out { animation: lc-mock-out 180ms ease-in forwards; }
|
|
676
|
+
.lc-mock-toast--default { background: #18224E; color: #fff; }
|
|
677
|
+
.lc-mock-toast--positive { background: #ECFDF5; color: #065F46; border: 1px solid #A7F3D0; }
|
|
678
|
+
.lc-mock-badge {
|
|
679
|
+
display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 5px;
|
|
680
|
+
font-size: 11px; letter-spacing: 0.02em; text-transform: uppercase; opacity: 0.72;
|
|
681
|
+
}
|
|
682
|
+
.lc-mock-badge-name { font-weight: 600; }
|
|
683
|
+
.lc-mock-pill { font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 999px; background: rgba(255,255,255,0.16); }
|
|
684
|
+
.lc-mock-toast--positive .lc-mock-pill { background: rgba(6,95,70,0.12); }
|
|
685
|
+
.lc-mock-body strong { font-weight: 700; }
|
|
686
|
+
.lc-mock-count { margin-left: 6px; font-weight: 700; opacity: 0.75; }
|
|
687
|
+
`.trim();
|
|
688
|
+
document.head.appendChild(style);
|
|
689
|
+
this.styleEl = style;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
34
693
|
var __defProp = Object.defineProperty;
|
|
35
694
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
36
695
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
37
696
|
const SYNC_STATUS_POLL_MAX_DURATION_MS = 10 * 60 * 1e3;
|
|
697
|
+
const DEFAULT_HOST_PROBE_TIMEOUT_MS = 1500;
|
|
698
|
+
const isLocalDevHost = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1" || hostname.endsWith(".localhost") || hostname.endsWith(".local");
|
|
699
|
+
function isEmbedded() {
|
|
700
|
+
if (typeof window === "undefined") return false;
|
|
701
|
+
try {
|
|
702
|
+
return window.self !== window.top;
|
|
703
|
+
} catch {
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
38
707
|
const _PartnerConnect = class _PartnerConnect {
|
|
39
708
|
constructor(options) {
|
|
40
709
|
__publicField(this, "hostOrigins", ["https://learncard.app"]);
|
|
@@ -47,7 +716,15 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
47
716
|
__publicField(this, "isInitialized", false);
|
|
48
717
|
__publicField(this, "syncCompleteCallbacks", /* @__PURE__ */ new Set());
|
|
49
718
|
__publicField(this, "syncStatusPollId", null);
|
|
50
|
-
|
|
719
|
+
__publicField(this, "mockHost", null);
|
|
720
|
+
__publicField(this, "embedded", false);
|
|
721
|
+
__publicField(this, "warnedNoHost", false);
|
|
722
|
+
__publicField(this, "hostProbeTimeout", DEFAULT_HOST_PROBE_TIMEOUT_MS);
|
|
723
|
+
/** Whether a real LearnCard host is believed to be listening. */
|
|
724
|
+
__publicField(this, "hostReachable", false);
|
|
725
|
+
/** Pending probe decision; requests queue behind it when set. */
|
|
726
|
+
__publicField(this, "activation", null);
|
|
727
|
+
var _a, _b, _c;
|
|
51
728
|
const hostOrigin = (_a = options == null ? void 0 : options.hostOrigin) != null ? _a : _PartnerConnect.DEFAULT_HOST_ORIGIN;
|
|
52
729
|
const configured = Array.isArray(hostOrigin) ? hostOrigin : [hostOrigin];
|
|
53
730
|
const disableDefaults = (options == null ? void 0 : options.disableDefaultTenants) === true;
|
|
@@ -56,9 +733,108 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
56
733
|
this.protocol = (options == null ? void 0 : options.protocol) || "LEARNCARD_V1";
|
|
57
734
|
this.requestTimeout = (options == null ? void 0 : options.requestTimeout) || 3e4;
|
|
58
735
|
this.allowNativeAppOrigins = (_b = options == null ? void 0 : options.allowNativeAppOrigins) != null ? _b : true;
|
|
736
|
+
this.hostProbeTimeout = (_c = options == null ? void 0 : options.hostProbeTimeout) != null ? _c : DEFAULT_HOST_PROBE_TIMEOUT_MS;
|
|
59
737
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
60
738
|
this.configureActiveOrigin();
|
|
61
739
|
this.setupMessageListener();
|
|
740
|
+
this.embedded = isEmbedded();
|
|
741
|
+
this.configureMockActivation(options);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Decide whether this instance talks to a real host, simulates one, or
|
|
745
|
+
* fails fast — the resolution of the `mock` option against the runtime
|
|
746
|
+
* embed context. See the `mock` option docs for the contract.
|
|
747
|
+
*/
|
|
748
|
+
configureMockActivation(options) {
|
|
749
|
+
var _a;
|
|
750
|
+
const mockSetting = (_a = options == null ? void 0 : options.mock) != null ? _a : "auto";
|
|
751
|
+
if (mockSetting === true) {
|
|
752
|
+
this.mockHost = new MockHost(options == null ? void 0 : options.mockOptions);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const canAutoMock = mockSetting === "standalone" || mockSetting === "auto" && this.isLocalDevContext();
|
|
756
|
+
if (!this.embedded) {
|
|
757
|
+
if (canAutoMock) {
|
|
758
|
+
this.mockHost = new MockHost(options == null ? void 0 : options.mockOptions);
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
switch (this.classifyParent()) {
|
|
763
|
+
case "learncard":
|
|
764
|
+
this.hostReachable = true;
|
|
765
|
+
return;
|
|
766
|
+
case "foreign":
|
|
767
|
+
if (canAutoMock) {
|
|
768
|
+
this.mockHost = new MockHost(options == null ? void 0 : options.mockOptions);
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
case "ambiguous":
|
|
772
|
+
if (canAutoMock) {
|
|
773
|
+
this.hostReachable = true;
|
|
774
|
+
this.activation = this.probeHost(options);
|
|
775
|
+
} else {
|
|
776
|
+
this.hostReachable = true;
|
|
777
|
+
}
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
isLocalDevContext() {
|
|
782
|
+
if (typeof window === "undefined") return false;
|
|
783
|
+
return isLocalDevHost(window.location.hostname);
|
|
784
|
+
}
|
|
785
|
+
classifyParent() {
|
|
786
|
+
const ancestorOrigin = this.readAncestorOrigin();
|
|
787
|
+
if (!ancestorOrigin) return "ambiguous";
|
|
788
|
+
if (this.matchesConfiguredOrigin(ancestorOrigin)) return "learncard";
|
|
789
|
+
if (this.allowNativeAppOrigins && this.isOriginNativeApp(ancestorOrigin)) {
|
|
790
|
+
return "ambiguous";
|
|
791
|
+
}
|
|
792
|
+
return "foreign";
|
|
793
|
+
}
|
|
794
|
+
matchesConfiguredOrigin(origin) {
|
|
795
|
+
return this.hostOrigins.some((entry) => _PartnerConnect.matchesOriginPattern(origin, entry));
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* One-time host presence probe for the ambiguous-parent case. Sends a
|
|
799
|
+
* side-effect-free `GET_SYNC_STATUS`; an answer proves a live LearnCard
|
|
800
|
+
* host (responses are origin-checked), a timeout means nobody is
|
|
801
|
+
* listening and the mock takes over. Requests issued while the probe is
|
|
802
|
+
* in flight queue behind the decision instead of racing it.
|
|
803
|
+
*/
|
|
804
|
+
probeHost(options) {
|
|
805
|
+
return this.postToHost("GET_SYNC_STATUS", void 0, this.hostProbeTimeout).then(() => {
|
|
806
|
+
this.activation = null;
|
|
807
|
+
}).catch(() => {
|
|
808
|
+
this.activation = null;
|
|
809
|
+
if (this.isInitialized) {
|
|
810
|
+
this.hostReachable = false;
|
|
811
|
+
this.mockHost = new MockHost(options == null ? void 0 : options.mockOptions);
|
|
812
|
+
console.warn(
|
|
813
|
+
`[LearnCard SDK] Embedded in a frame, but no LearnCard host answered within ${this.hostProbeTimeout}ms \u2014 activating standalone mock mode. If a real local host was just slow to boot, raise \`hostProbeTimeout\`.`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Whether this SDK instance is running inside an embedded iframe.
|
|
820
|
+
* Instance-level convenience wrapper around the standalone {@link isEmbedded}.
|
|
821
|
+
*/
|
|
822
|
+
isEmbedded() {
|
|
823
|
+
return isEmbedded();
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Whether the current page is running inside an embedded iframe.
|
|
827
|
+
* Static convenience wrapper around the standalone {@link isEmbedded}.
|
|
828
|
+
*/
|
|
829
|
+
static isEmbedded() {
|
|
830
|
+
return isEmbedded();
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Whether this instance is currently simulating the LearnCard host locally
|
|
834
|
+
* instead of talking to a real host over `postMessage`.
|
|
835
|
+
*/
|
|
836
|
+
isMocked() {
|
|
837
|
+
return this.mockHost !== null;
|
|
62
838
|
}
|
|
63
839
|
/**
|
|
64
840
|
* Read `window.location.ancestorOrigins[0]` without throwing if the
|
|
@@ -226,6 +1002,9 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
226
1002
|
if (data.protocol !== this.protocol || !data.requestId) {
|
|
227
1003
|
return;
|
|
228
1004
|
}
|
|
1005
|
+
if (data.type !== "SUCCESS" && data.type !== "ERROR") {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
229
1008
|
const pending = this.pendingRequests.get(data.requestId);
|
|
230
1009
|
if (!pending) {
|
|
231
1010
|
return;
|
|
@@ -255,14 +1034,44 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
255
1034
|
return `${action}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
256
1035
|
}
|
|
257
1036
|
/**
|
|
258
|
-
* Send a message to the parent window and return a Promise
|
|
1037
|
+
* Send a message to the parent window and return a Promise. While a host
|
|
1038
|
+
* presence probe is pending, requests queue behind its decision so they
|
|
1039
|
+
* are answered by whichever side (real host or mock) actually exists.
|
|
259
1040
|
*/
|
|
260
1041
|
sendMessage(action, payload) {
|
|
1042
|
+
if (this.activation) {
|
|
1043
|
+
return this.activation.then(() => this.dispatchMessage(action, payload));
|
|
1044
|
+
}
|
|
1045
|
+
return this.dispatchMessage(action, payload);
|
|
1046
|
+
}
|
|
1047
|
+
dispatchMessage(action, payload) {
|
|
261
1048
|
if (!this.isInitialized) {
|
|
262
1049
|
return Promise.reject(
|
|
263
1050
|
new PartnerConnectError("SDK_NOT_INITIALIZED", "SDK is not initialized")
|
|
264
1051
|
);
|
|
265
1052
|
}
|
|
1053
|
+
if (this.mockHost) {
|
|
1054
|
+
return this.mockHost.handle(action, payload).then((data) => data).catch((error) => {
|
|
1055
|
+
throw PartnerConnectError.from(error);
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
if (!this.hostReachable) {
|
|
1059
|
+
if (!this.warnedNoHost) {
|
|
1060
|
+
this.warnedNoHost = true;
|
|
1061
|
+
console.error(
|
|
1062
|
+
"[LearnCard SDK] No LearnCard host is present (the app is standalone or inside a non-LearnCard frame), so SDK calls cannot complete. Embed your app in LearnCard, or pass `mock: true` to simulate the host during standalone development. Use isEmbedded() to branch your UI before calling."
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
return Promise.reject(
|
|
1066
|
+
new PartnerConnectError(
|
|
1067
|
+
"LC_NOT_EMBEDDED",
|
|
1068
|
+
`Cannot ${action}: the app is not embedded in a LearnCard host.`
|
|
1069
|
+
)
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
return this.postToHost(action, payload, this.requestTimeout);
|
|
1073
|
+
}
|
|
1074
|
+
postToHost(action, payload, timeout) {
|
|
266
1075
|
return new Promise((resolve, reject) => {
|
|
267
1076
|
const requestId = this.generateRequestId(action);
|
|
268
1077
|
const timeoutId = setTimeout(() => {
|
|
@@ -271,11 +1080,11 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
271
1080
|
reject(
|
|
272
1081
|
new PartnerConnectError(
|
|
273
1082
|
"LC_TIMEOUT",
|
|
274
|
-
`Request ${action} timed out after ${
|
|
1083
|
+
`Request ${action} timed out after ${timeout}ms`
|
|
275
1084
|
)
|
|
276
1085
|
);
|
|
277
1086
|
}
|
|
278
|
-
},
|
|
1087
|
+
}, timeout);
|
|
279
1088
|
this.pendingRequests.set(requestId, {
|
|
280
1089
|
resolve,
|
|
281
1090
|
reject,
|
|
@@ -756,6 +1565,10 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
756
1565
|
this.syncStatusPollId = null;
|
|
757
1566
|
}
|
|
758
1567
|
this.syncCompleteCallbacks.clear();
|
|
1568
|
+
if (this.mockHost) {
|
|
1569
|
+
this.mockHost.destroy();
|
|
1570
|
+
this.mockHost = null;
|
|
1571
|
+
}
|
|
759
1572
|
this.isInitialized = false;
|
|
760
1573
|
}
|
|
761
1574
|
};
|
|
@@ -810,5 +1623,5 @@ function createPartnerConnect(options) {
|
|
|
810
1623
|
return new PartnerConnect(options);
|
|
811
1624
|
}
|
|
812
1625
|
|
|
813
|
-
export { PartnerConnect, PartnerConnectError, createPartnerConnect, createPartnerConnect as default };
|
|
1626
|
+
export { PartnerConnect, PartnerConnectError, createPartnerConnect, createPartnerConnect as default, isEmbedded };
|
|
814
1627
|
//# sourceMappingURL=partner-connect.mjs.map
|