@blux.ai/web-sdk 2.2.8 → 2.2.10
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/context.md +41 -0
- package/dist/package.json +1 -1
- package/dist/src/BluxClient.d.ts +50 -0
- package/dist/src/BluxClient.js +133 -10
- package/dist/src/BluxClient.js.map +1 -1
- package/dist/src/bridges/AndroidBridge.d.ts +11 -0
- package/dist/src/bridges/AndroidBridge.js +39 -0
- package/dist/src/bridges/AndroidBridge.js.map +1 -0
- package/dist/src/bridges/Bridge.d.ts +2 -1
- package/dist/src/bridges/Bridge.js +8 -0
- package/dist/src/bridges/Bridge.js.map +1 -1
- package/dist/src/bridges/IosBridge.d.ts +11 -0
- package/dist/src/bridges/IosBridge.js +40 -0
- package/dist/src/bridges/IosBridge.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/context.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Web SDK Context
|
|
2
|
+
|
|
3
|
+
## In-App Message 렌더링
|
|
4
|
+
|
|
5
|
+
### Blob URL vs srcdoc
|
|
6
|
+
|
|
7
|
+
iframe 기반 인앱메시지 렌더링 시 `srcdoc` 대신 **Blob URL** 방식을 사용한다:
|
|
8
|
+
|
|
9
|
+
- `srcdoc`: 이미 파싱된 HTML을 받아서 스크립트가 정적으로 삽입됨. Next.js hydration 실패
|
|
10
|
+
- `Blob URL`: 실제 URL처럼 동작하여 스크립트가 순차적으로 파싱/실행됨. iOS `loadHTMLString`과 유사
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// srcdoc (X) - hydration 실패
|
|
14
|
+
iframe.srcdoc = htmlString;
|
|
15
|
+
|
|
16
|
+
// Blob URL (O) - 정상 동작
|
|
17
|
+
const blob = new Blob([htmlString], { type: "text/html" });
|
|
18
|
+
iframe.src = URL.createObjectURL(blob);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### History API Polyfill
|
|
22
|
+
|
|
23
|
+
Blob URL 환경에서 `history.replaceState/pushState` 호출 시 SecurityError가 발생할 수 있다. polyfill을 `<head>`에 주입하여 해결:
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
(function () {
|
|
27
|
+
var origReplaceState = history.replaceState;
|
|
28
|
+
history.replaceState = function () {
|
|
29
|
+
try {
|
|
30
|
+
return origReplaceState.apply(this, arguments);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.debug("[Blux] history.replaceState blocked");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
// pushState도 동일하게 처리
|
|
36
|
+
})();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 참고 구현
|
|
40
|
+
|
|
41
|
+
- `src/BluxClient.ts` - `displayInapp()` 메서드
|
package/dist/package.json
CHANGED
package/dist/src/BluxClient.d.ts
CHANGED
|
@@ -35,6 +35,19 @@ type UserPropertiesQueueItem = {
|
|
|
35
35
|
resolve: () => void;
|
|
36
36
|
reject: (error: unknown) => void;
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* 인앱 메시지에서 BluxBridge.triggerAction() 호출 시 전달되는 이벤트
|
|
40
|
+
*/
|
|
41
|
+
export type InAppCustomActionEvent = {
|
|
42
|
+
/** 액션 식별자 */
|
|
43
|
+
actionId: string;
|
|
44
|
+
/** 액션에 전달된 데이터 */
|
|
45
|
+
data: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* 인앱 커스텀 액션 핸들러 타입
|
|
49
|
+
*/
|
|
50
|
+
export type InAppCustomActionHandler = (event: InAppCustomActionEvent) => void | Promise<void>;
|
|
38
51
|
export declare class BluxClient {
|
|
39
52
|
private readonly api;
|
|
40
53
|
private readonly sdkInfo;
|
|
@@ -64,6 +77,8 @@ export declare class BluxClient {
|
|
|
64
77
|
private readonly intersectionObserver;
|
|
65
78
|
private readonly intersectionEntry$;
|
|
66
79
|
private hitElements;
|
|
80
|
+
private currentInappCleanup;
|
|
81
|
+
private inAppCustomActionHandlers;
|
|
67
82
|
private resolveBridge;
|
|
68
83
|
constructor({ bluxApplicationId, bluxAPIKey, ...bridgeOptions }: {
|
|
69
84
|
bluxApplicationId: string;
|
|
@@ -89,6 +104,41 @@ export declare class BluxClient {
|
|
|
89
104
|
customUserProperties: NonNullable<APIs.bluxUsersUpdateProperties.Body["custom_user_properties"]>;
|
|
90
105
|
}): Promise<void>;
|
|
91
106
|
sendEvent(event: BluxEvent | BluxEvent[]): void;
|
|
107
|
+
/**
|
|
108
|
+
* 현재 표시 중인 인앱 메시지를 프로그래밍 방식으로 닫습니다.
|
|
109
|
+
*
|
|
110
|
+
* Custom HTML 인앱에서 async 핸들러 완료 후 수동으로 닫을 때 사용합니다.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* // BluxBridge.triggerAction('share', { url: '...' }, false); // shouldDismiss: false
|
|
115
|
+
* // 핸들러에서 async 작업 완료 후:
|
|
116
|
+
* bluxClient.dismissInApp();
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
dismissInApp(): void;
|
|
120
|
+
/**
|
|
121
|
+
* Custom HTML 인앱에서 BluxBridge.triggerAction() 호출 시 실행될 핸들러를 등록합니다.
|
|
122
|
+
* 여러 핸들러를 등록할 수 있으며, 등록 순서대로 실행됩니다.
|
|
123
|
+
*
|
|
124
|
+
* @param handler - 커스텀 액션 핸들러 함수
|
|
125
|
+
* @returns 핸들러를 제거하는 unsubscribe 함수
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* const unsubscribe = bluxClient.addInAppCustomActionHandler(async (event) => {
|
|
130
|
+
* if (event.actionId === 'share') {
|
|
131
|
+
* await navigator.share({ url: event.data.url as string });
|
|
132
|
+
* // async 작업 완료 후 수동으로 닫기 (shouldDismiss: false로 호출한 경우)
|
|
133
|
+
* bluxClient.dismissInApp();
|
|
134
|
+
* }
|
|
135
|
+
* });
|
|
136
|
+
*
|
|
137
|
+
* // 핸들러 제거
|
|
138
|
+
* unsubscribe();
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
addInAppCustomActionHandler(handler: InAppCustomActionHandler): () => void;
|
|
92
142
|
private urlAndRef$;
|
|
93
143
|
private handleUrlAndRef;
|
|
94
144
|
private registerImpressionTracker;
|
package/dist/src/BluxClient.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import ObjectId from "bson-objectid";
|
|
3
3
|
import dayjs from "dayjs";
|
|
4
|
-
import { BehaviorSubject, bufferTime, catchError, combineLatest, concatMap, debounceTime, filter, first, firstValueFrom, from, groupBy, merge, mergeMap, NEVER, skip, Subject, switchMap, throwError,
|
|
4
|
+
import { BehaviorSubject, bufferTime, catchError, combineLatest, concatMap, debounceTime, filter, first, firstValueFrom, from, groupBy, map, merge, mergeMap, NEVER, skip, Subject, switchMap, throwError, timeout, timer, } from "rxjs";
|
|
5
5
|
import packageJson from "../package.json";
|
|
6
6
|
import { DevicePlatform } from "./apis/APIs";
|
|
7
7
|
import { createCrmEvent } from "./apis/createCrmEvent";
|
|
@@ -12,7 +12,9 @@ import { notificationsUpdate } from "./apis/notificationsUpdate";
|
|
|
12
12
|
import { signIn } from "./apis/signIn";
|
|
13
13
|
import { signOut } from "./apis/signOut";
|
|
14
14
|
import { updateProperties } from "./apis/updateProperties";
|
|
15
|
+
import { AndroidBridge } from "./bridges/AndroidBridge";
|
|
15
16
|
import { FlutterBridge } from "./bridges/FlutterBridge";
|
|
17
|
+
import { IosBridge } from "./bridges/IosBridge";
|
|
16
18
|
import { RNBridge } from "./bridges/RNBridge";
|
|
17
19
|
import { BLUX_ATTRIBUTES } from "./constants/BLUX_ATTRIBUTES";
|
|
18
20
|
import { AddClickEvent, AddPageVisitEvent, AddSectionViewEvent, VisitEvent, } from "./events";
|
|
@@ -55,6 +57,10 @@ export class BluxClient {
|
|
|
55
57
|
intersectionEntry$ = new Subject();
|
|
56
58
|
// Hit 관련 변수
|
|
57
59
|
hitElements = [];
|
|
60
|
+
// 현재 표시 중인 인앱 메시지의 cleanup 함수
|
|
61
|
+
currentInappCleanup = null;
|
|
62
|
+
// 인앱 커스텀 액션 핸들러 (복수 등록 가능)
|
|
63
|
+
inAppCustomActionHandlers = new Set();
|
|
58
64
|
resolveBridge(bridgeOptions) {
|
|
59
65
|
if ("useRNBridge" in bridgeOptions && bridgeOptions.useRNBridge) {
|
|
60
66
|
return new RNBridge();
|
|
@@ -63,6 +69,10 @@ export class BluxClient {
|
|
|
63
69
|
switch (bridgeOptions.bridgePlatform) {
|
|
64
70
|
case "none":
|
|
65
71
|
return null;
|
|
72
|
+
case "android":
|
|
73
|
+
return new AndroidBridge();
|
|
74
|
+
case "ios":
|
|
75
|
+
return new IosBridge();
|
|
66
76
|
case "react-native":
|
|
67
77
|
return new RNBridge();
|
|
68
78
|
case "flutter":
|
|
@@ -213,15 +223,31 @@ export class BluxClient {
|
|
|
213
223
|
if (isShouldSkip)
|
|
214
224
|
return resolve(true);
|
|
215
225
|
const iframeElement = document.createElement("iframe");
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
226
|
+
// Blob URL 방식: 실제 URL 컨텍스트를 가지므로 Next.js가 정상 작동
|
|
227
|
+
// <base> 태그로 외부 리소스 경로 설정
|
|
228
|
+
// history.replaceState/pushState 에러 방지를 위한 polyfill 추가
|
|
229
|
+
const historyPolyfill = `<script>
|
|
230
|
+
(function() {
|
|
231
|
+
var origReplaceState = history.replaceState;
|
|
232
|
+
var origPushState = history.pushState;
|
|
233
|
+
history.replaceState = function() {
|
|
234
|
+
try { return origReplaceState.apply(this, arguments); }
|
|
235
|
+
catch(e) { console.debug('[Blux] history.replaceState blocked in blob context'); }
|
|
236
|
+
};
|
|
237
|
+
history.pushState = function() {
|
|
238
|
+
try { return origPushState.apply(this, arguments); }
|
|
239
|
+
catch(e) { console.debug('[Blux] history.pushState blocked in blob context'); }
|
|
240
|
+
};
|
|
241
|
+
})();
|
|
242
|
+
</script>`;
|
|
243
|
+
const headInjection = `${historyPolyfill}<base href="${inapp.baseUrl}/">`;
|
|
244
|
+
const htmlWithBase = inapp.htmlString.includes("<head>")
|
|
245
|
+
? inapp.htmlString.replace("<head>", `<head>${headInjection}`)
|
|
246
|
+
: inapp.htmlString;
|
|
247
|
+
const blob = new Blob([htmlWithBase], { type: "text/html" });
|
|
248
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
249
|
+
iframeElement.sandbox.add("allow-scripts", "allow-same-origin", "allow-forms");
|
|
250
|
+
iframeElement.src = blobUrl;
|
|
225
251
|
iframeElement.style.position = "fixed";
|
|
226
252
|
iframeElement.style.width = `${window.innerWidth}px`;
|
|
227
253
|
iframeElement.style.height = `${window.innerHeight}px`;
|
|
@@ -232,13 +258,17 @@ export class BluxClient {
|
|
|
232
258
|
document.body.appendChild(iframeElement);
|
|
233
259
|
let urlChangeSubscription = null;
|
|
234
260
|
const cleanup = () => {
|
|
261
|
+
URL.revokeObjectURL(blobUrl);
|
|
235
262
|
iframeElement.remove();
|
|
236
263
|
window.removeEventListener("message", handleMessage);
|
|
237
264
|
window.visualViewport?.removeEventListener("resize", handleViewportChange);
|
|
238
265
|
window.visualViewport?.removeEventListener("scroll", handleViewportChange);
|
|
239
266
|
urlChangeSubscription?.unsubscribe();
|
|
267
|
+
this.currentInappCleanup = null;
|
|
240
268
|
resolve(inapp.inappId);
|
|
241
269
|
};
|
|
270
|
+
// 현재 인앱 cleanup 함수 저장
|
|
271
|
+
this.currentInappCleanup = cleanup;
|
|
242
272
|
const handleViewportChange = () => {
|
|
243
273
|
const width = window.visualViewport?.width ?? window.innerWidth;
|
|
244
274
|
const height = window.visualViewport?.height ?? window.innerHeight;
|
|
@@ -253,6 +283,26 @@ export class BluxClient {
|
|
|
253
283
|
if (event.source !== iframeElement.contentWindow)
|
|
254
284
|
return;
|
|
255
285
|
const message = event.data;
|
|
286
|
+
if (message.action === "resize") {
|
|
287
|
+
const { height, location } = message.data;
|
|
288
|
+
// 배너용 iframe 스타일 조정
|
|
289
|
+
iframeElement.style.width = "90vw";
|
|
290
|
+
iframeElement.style.maxWidth = "448px";
|
|
291
|
+
iframeElement.style.height = `${height}px`;
|
|
292
|
+
iframeElement.style.left = "50%";
|
|
293
|
+
iframeElement.style.transform = "translateX(-50%)";
|
|
294
|
+
if (location === "top") {
|
|
295
|
+
iframeElement.style.top = "0";
|
|
296
|
+
iframeElement.style.bottom = "auto";
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
iframeElement.style.top = "auto";
|
|
300
|
+
iframeElement.style.bottom = "0";
|
|
301
|
+
}
|
|
302
|
+
window.visualViewport?.removeEventListener("resize", handleViewportChange);
|
|
303
|
+
window.visualViewport?.removeEventListener("scroll", handleViewportChange);
|
|
304
|
+
return; // resize는 cleanup하지 않음
|
|
305
|
+
}
|
|
256
306
|
if (message.action === "hide") {
|
|
257
307
|
const { days_to_hide } = message.data;
|
|
258
308
|
if (days_to_hide !== 0) {
|
|
@@ -304,6 +354,37 @@ export class BluxClient {
|
|
|
304
354
|
// opened_by_iframe && open_in_new_tab인 경우: iframe에서 이미 새 탭 열었으므로 스킵
|
|
305
355
|
}
|
|
306
356
|
}
|
|
357
|
+
else if (message.action === "custom_action") {
|
|
358
|
+
const { action_id, data, should_dismiss } = message.data;
|
|
359
|
+
// 등록된 모든 핸들러 호출
|
|
360
|
+
for (const handler of this.inAppCustomActionHandlers) {
|
|
361
|
+
try {
|
|
362
|
+
handler({ actionId: action_id, data });
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
Logger.error(error, {
|
|
366
|
+
tags: { from: "inAppCustomActionHandler" },
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// should_dismiss가 false면 cleanup하지 않음
|
|
371
|
+
if (!should_dismiss) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else if (message.action === "inapp_opened") {
|
|
376
|
+
// data-blux-click 속성으로 인한 클릭 이벤트 추적
|
|
377
|
+
if (bluxUser) {
|
|
378
|
+
this.enqueueRequest(() => createCrmEvent(this.api, {
|
|
379
|
+
application_id: this.bluxApplicationId,
|
|
380
|
+
}, {
|
|
381
|
+
notification_id: inapp.notificationId,
|
|
382
|
+
crm_event_type: "inapp_opened",
|
|
383
|
+
captured_at: new Date().toISOString(),
|
|
384
|
+
}, this.generateApiHeader(bluxUser)));
|
|
385
|
+
}
|
|
386
|
+
return; // 인앱메시지를 닫지 않음
|
|
387
|
+
}
|
|
307
388
|
cleanup();
|
|
308
389
|
};
|
|
309
390
|
// URL 변경 시 인앱메시지 닫기 (SPA 뒤로가기/앞으로가기 대응)
|
|
@@ -511,6 +592,48 @@ export class BluxClient {
|
|
|
511
592
|
Logger.error(error, { tags: { from: "sendEvent" } });
|
|
512
593
|
}
|
|
513
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* 현재 표시 중인 인앱 메시지를 프로그래밍 방식으로 닫습니다.
|
|
597
|
+
*
|
|
598
|
+
* Custom HTML 인앱에서 async 핸들러 완료 후 수동으로 닫을 때 사용합니다.
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* ```typescript
|
|
602
|
+
* // BluxBridge.triggerAction('share', { url: '...' }, false); // shouldDismiss: false
|
|
603
|
+
* // 핸들러에서 async 작업 완료 후:
|
|
604
|
+
* bluxClient.dismissInApp();
|
|
605
|
+
* ```
|
|
606
|
+
*/
|
|
607
|
+
dismissInApp() {
|
|
608
|
+
this.currentInappCleanup?.();
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Custom HTML 인앱에서 BluxBridge.triggerAction() 호출 시 실행될 핸들러를 등록합니다.
|
|
612
|
+
* 여러 핸들러를 등록할 수 있으며, 등록 순서대로 실행됩니다.
|
|
613
|
+
*
|
|
614
|
+
* @param handler - 커스텀 액션 핸들러 함수
|
|
615
|
+
* @returns 핸들러를 제거하는 unsubscribe 함수
|
|
616
|
+
*
|
|
617
|
+
* @example
|
|
618
|
+
* ```typescript
|
|
619
|
+
* const unsubscribe = bluxClient.addInAppCustomActionHandler(async (event) => {
|
|
620
|
+
* if (event.actionId === 'share') {
|
|
621
|
+
* await navigator.share({ url: event.data.url as string });
|
|
622
|
+
* // async 작업 완료 후 수동으로 닫기 (shouldDismiss: false로 호출한 경우)
|
|
623
|
+
* bluxClient.dismissInApp();
|
|
624
|
+
* }
|
|
625
|
+
* });
|
|
626
|
+
*
|
|
627
|
+
* // 핸들러 제거
|
|
628
|
+
* unsubscribe();
|
|
629
|
+
* ```
|
|
630
|
+
*/
|
|
631
|
+
addInAppCustomActionHandler(handler) {
|
|
632
|
+
this.inAppCustomActionHandlers.add(handler);
|
|
633
|
+
return () => {
|
|
634
|
+
this.inAppCustomActionHandlers.delete(handler);
|
|
635
|
+
};
|
|
636
|
+
}
|
|
514
637
|
urlAndRef$ = new BehaviorSubject(undefined);
|
|
515
638
|
handleUrlAndRef() {
|
|
516
639
|
// set url and ref to local storage
|