@apps-in-toss/web-bridge 2.9.3 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1035,6 +1035,197 @@ function resetAttachedBannerRegistry() {
1035
1035
  attachedBannerRegistry = /* @__PURE__ */ new WeakMap();
1036
1036
  }
1037
1037
 
1038
+ // src/toss-ad/constants.ts
1039
+ var ERROR_CODES = {
1040
+ NETWORK_ERROR: 1001,
1041
+ INVALID_REQUEST: 1002,
1042
+ NO_AD: 1003,
1043
+ ERROR: 1004,
1044
+ INVALID_SPACE: 1005,
1045
+ SDK_NOT_INITIALIZED: 1007,
1046
+ HTTP_TIMEOUT: 1008,
1047
+ EXECUTION_FAIL: 1009,
1048
+ INTERRUPTED: 1010,
1049
+ FAIL: 1011,
1050
+ BLOCKED: 1012,
1051
+ TIMEOUT: 1013,
1052
+ LIMITED_AD: 1014,
1053
+ CONSENT_REQUIRED: 1015,
1054
+ TEST_MODE: 1016,
1055
+ INTERNAL_ERROR: 1017
1056
+ };
1057
+ var NON_ERROR_STATUSES = /* @__PURE__ */ new Set(["OK", "TEST_MODE", "NO_AD"]);
1058
+ var API_RESULT_ERROR_CODE_MAP = {
1059
+ HTTP_TIMEOUT: ERROR_CODES.HTTP_TIMEOUT,
1060
+ NETWORK_ERROR: ERROR_CODES.NETWORK_ERROR,
1061
+ EXECUTION_FAIL: ERROR_CODES.EXECUTION_FAIL,
1062
+ INTERRUPTED: ERROR_CODES.INTERRUPTED,
1063
+ INTERNAL_ERROR: ERROR_CODES.ERROR,
1064
+ FAIL: ERROR_CODES.FAIL
1065
+ };
1066
+ var AD_STATUS_ERROR_CODE_MAP = {
1067
+ BLOCKED: ERROR_CODES.BLOCKED,
1068
+ ERROR: ERROR_CODES.ERROR,
1069
+ TIMEOUT: ERROR_CODES.TIMEOUT,
1070
+ INVALID_SPACE: ERROR_CODES.INVALID_SPACE,
1071
+ INVALID_REQUEST: ERROR_CODES.INVALID_REQUEST,
1072
+ LIMITED_AD: ERROR_CODES.LIMITED_AD,
1073
+ CONSENT_REQUIRED: ERROR_CODES.CONSENT_REQUIRED
1074
+ };
1075
+
1076
+ // src/toss-ad/tossAdEventLog.ts
1077
+ var tossAdEventLog = createAsyncBridge("tossAdEventLog");
1078
+
1079
+ // src/toss-ad/errorTracking.ts
1080
+ var BANNER_ERROR_LOG_NAME = "display_ads_all::error__banner";
1081
+ function logBannerErrorToAppLog(info) {
1082
+ void tossAdEventLog({
1083
+ log_name: BANNER_ERROR_LOG_NAME,
1084
+ log_type: "debug",
1085
+ params: {
1086
+ error_code: info.errorCode,
1087
+ error_message: info.errorMessage,
1088
+ ad_group_id: info.adGroupId,
1089
+ mediation_id: info.mediationId,
1090
+ event_context_token: info.eventContextToken
1091
+ }
1092
+ }).catch(() => {
1093
+ });
1094
+ }
1095
+ function trackBannerError(info) {
1096
+ if (info.endpointError) {
1097
+ void fetch(info.endpointError, {
1098
+ method: "POST",
1099
+ headers: { "Content-Type": "application/json" },
1100
+ body: JSON.stringify({
1101
+ eventContextToken: info.eventContextToken,
1102
+ mediationId: info.mediationId,
1103
+ errorCode: info.errorCode,
1104
+ errorMessage: info.errorMessage
1105
+ })
1106
+ // POST 실패(CORS·네트워크) → 앱로그 fallback (endpoint와 같은 채널이라 함께 막히는 경우 대비).
1107
+ }).catch(() => logBannerErrorToAppLog(info));
1108
+ return;
1109
+ }
1110
+ logBannerErrorToAppLog(info);
1111
+ }
1112
+ function trackResponseError(response, adGroupId, context) {
1113
+ if (response.resultType !== "SUCCESS") {
1114
+ trackBannerError({
1115
+ adGroupId,
1116
+ errorCode: API_RESULT_ERROR_CODE_MAP[response.resultType] ?? ERROR_CODES.INTERNAL_ERROR,
1117
+ errorMessage: response.error?.reason ?? response.resultType
1118
+ });
1119
+ return;
1120
+ }
1121
+ const success = response.success;
1122
+ if (!success || NON_ERROR_STATUSES.has(String(success.status))) {
1123
+ return;
1124
+ }
1125
+ trackBannerError({
1126
+ adGroupId,
1127
+ errorCode: AD_STATUS_ERROR_CODE_MAP[String(success.status)] ?? ERROR_CODES.INTERNAL_ERROR,
1128
+ mediationId: context.mediationId,
1129
+ eventContextToken: context.eventContextToken,
1130
+ errorMessage: `Ad response status: ${success.status}`,
1131
+ endpointError: context.errorEndpoint
1132
+ });
1133
+ }
1134
+
1135
+ // src/toss-ad/eventTracking.ts
1136
+ async function handleTrackingRequest(request) {
1137
+ if (request.type === "EVENT_TRACKING") {
1138
+ await sendEventTracking(request);
1139
+ }
1140
+ }
1141
+ async function sendEventTracking(request) {
1142
+ if (request.urls.length === 0) {
1143
+ return;
1144
+ }
1145
+ const body = JSON.stringify({ type: request.eventName, data: request.eventPayload });
1146
+ const headers = { "Content-Type": "application/json" };
1147
+ if (request.requestId) {
1148
+ headers["X-Toss-RequestId"] = request.requestId;
1149
+ }
1150
+ const settled = await Promise.allSettled(
1151
+ request.urls.map(
1152
+ (url) => fetch(url, { method: "POST", headers, body }).then((response) => {
1153
+ if (!response.ok) {
1154
+ throw new Error(`HTTP ${response.status}`);
1155
+ }
1156
+ })
1157
+ )
1158
+ );
1159
+ const failedCount = settled.filter((result) => result.status === "rejected").length;
1160
+ if (failedCount === 0) {
1161
+ return;
1162
+ }
1163
+ trackBannerError({
1164
+ adGroupId: request.adGroupId,
1165
+ errorCode: ERROR_CODES.NETWORK_ERROR,
1166
+ errorMessage: `EVENT_TRACKING failed: ${request.eventName} (${failedCount}/${request.urls.length})`,
1167
+ mediationId: request.mediationId || void 0,
1168
+ eventContextToken: request.eventContextToken || void 0,
1169
+ endpointError: request.errorEndpoint
1170
+ });
1171
+ }
1172
+
1173
+ // src/toss-ad/fetchAppsInTossAdSchema.ts
1174
+ var APPS_IN_TOSS_AD_SPEC_VERSION = "1.4.1";
1175
+ var FETCH_APPS_IN_TOSS_AD_DEVICE_MACRO = "{{device}}";
1176
+ var TOSS_APP_BUNDLE_IDS = {
1177
+ ios: "com.vivarepublica.cash",
1178
+ android: "viva.republica.toss"
1179
+ };
1180
+ function resolveTossAppBundleId(os) {
1181
+ return TOSS_APP_BUNDLE_IDS[os];
1182
+ }
1183
+
1184
+ // src/toss-ad/normalize.ts
1185
+ var LIST_BANNER_STYLE_ID = "1";
1186
+ var NATIVE_IMAGE_STYLE_ID = "2";
1187
+ var AVAILABLE_STYLE_IDS = [LIST_BANNER_STYLE_ID, NATIVE_IMAGE_STYLE_ID];
1188
+ var SUPPORTED_STYLE_IDS = new Set(AVAILABLE_STYLE_IDS);
1189
+ function normalizeAdResponse(adResponse) {
1190
+ const ads = Array.isArray(adResponse.ads) ? adResponse.ads.filter((ad) => SUPPORTED_STYLE_IDS.has(String(ad.styleId))) : [];
1191
+ return {
1192
+ requestId: adResponse.requestId ?? "",
1193
+ // SDK가 event 트래킹 위임(onTrackingRequest) 시 응답 top-level에서 읽어 실어 보낸다.
1194
+ // 떼어내면 SDK가 토큰을 못 보고, event 트래킹 실패 시 endpoint.error에서 토큰이 빠진다.
1195
+ eventContextToken: adResponse.eventContextToken,
1196
+ status: adResponse.status ?? "OK",
1197
+ ads,
1198
+ ext: adResponse.ext
1199
+ };
1200
+ }
1201
+ function normalizeApiResponse(raw) {
1202
+ if (isApiResponse(raw)) {
1203
+ if (raw.resultType !== "SUCCESS") {
1204
+ return raw;
1205
+ }
1206
+ if (!raw.success) {
1207
+ return {
1208
+ resultType: "FAIL",
1209
+ error: { reason: "fetchTossAd returned SUCCESS without payload" }
1210
+ };
1211
+ }
1212
+ return { ...raw, success: normalizeAdResponse(raw.success) };
1213
+ }
1214
+ if (isAdResponse(raw)) {
1215
+ return {
1216
+ resultType: "SUCCESS",
1217
+ success: normalizeAdResponse(raw)
1218
+ };
1219
+ }
1220
+ return { resultType: "FAIL", error: { reason: "Invalid response from fetchTossAd" } };
1221
+ }
1222
+ function isApiResponse(payload) {
1223
+ return Boolean(payload && typeof payload === "object" && "resultType" in payload);
1224
+ }
1225
+ function isAdResponse(payload) {
1226
+ return Boolean(payload && typeof payload === "object" && "ads" in payload);
1227
+ }
1228
+
1038
1229
  // src/toss-ad/opener.ts
1039
1230
  var openURL = createAsyncBridge("openURL");
1040
1231
  function openUrlOpener(url) {
@@ -1049,8 +1240,49 @@ function supertossWeb(uri) {
1049
1240
  return `supertoss://web?url=${encodeURIComponent(uri)}&external=true`;
1050
1241
  }
1051
1242
 
1243
+ // src/toss-ad/resultTracking.ts
1244
+ async function fireResultTracking(response, adGroupId, ctx) {
1245
+ const status = response.success?.status;
1246
+ if (response.resultType !== "SUCCESS" || status !== "OK" && status !== "TEST_MODE") {
1247
+ return;
1248
+ }
1249
+ if (ctx.resultEndpoint == null || ctx.mediationId == null) {
1250
+ return;
1251
+ }
1252
+ const body = {
1253
+ mediationId: ctx.mediationId,
1254
+ spaceUnitId: adGroupId,
1255
+ requestId: response.success?.requestId ?? "",
1256
+ eventContextToken: ctx.eventContextToken,
1257
+ winnerSource: "TOSS",
1258
+ content: null,
1259
+ adUnitId: ctx.adUnitId ?? null,
1260
+ placementId: ctx.placementId ?? null,
1261
+ tossFailed: null
1262
+ };
1263
+ try {
1264
+ const res = await fetch(ctx.resultEndpoint, {
1265
+ method: "POST",
1266
+ headers: { "Content-Type": "application/json" },
1267
+ body: JSON.stringify(body)
1268
+ });
1269
+ if (!res.ok) {
1270
+ throw new Error(`HTTP ${res.status}`);
1271
+ }
1272
+ } catch (error) {
1273
+ trackBannerError({
1274
+ adGroupId,
1275
+ errorCode: ERROR_CODES.NETWORK_ERROR,
1276
+ errorMessage: `RESULT_TRACKING failed: ${error instanceof Error ? error.message : String(error)}`,
1277
+ mediationId: ctx.mediationId,
1278
+ eventContextToken: ctx.eventContextToken,
1279
+ endpointError: ctx.errorEndpoint
1280
+ });
1281
+ }
1282
+ }
1283
+
1052
1284
  // src/toss-ad/scriptLoader.ts
1053
- var DEFAULT_SDK_URL = "https://static.toss.im/ads/sdk/toss-ads-space-kit-1.3.0.js";
1285
+ var DEFAULT_SDK_URL = "https://static.toss.im/ads/sdk/toss-ads-space-kit-3.1.0.js";
1054
1286
  var DEFAULT_TIMEOUT_MS = 15e3;
1055
1287
  var pendingLoad = null;
1056
1288
  function getAdsSdk() {
@@ -1106,12 +1338,79 @@ function loadAdsSdk() {
1106
1338
  return promise;
1107
1339
  }
1108
1340
 
1341
+ // package.json
1342
+ var package_default = {
1343
+ name: "@apps-in-toss/web-bridge",
1344
+ type: "module",
1345
+ version: "2.10.0",
1346
+ description: "Web Bridge for Apps In Toss",
1347
+ scripts: {
1348
+ typecheck: "tsc --noEmit",
1349
+ lint: "eslint .",
1350
+ build: "tsup"
1351
+ },
1352
+ main: "./dist/index.cjs",
1353
+ module: "./dist/index.js",
1354
+ types: "./dist/index.d.ts",
1355
+ exports: {
1356
+ ".": {
1357
+ import: {
1358
+ types: "./dist/index.d.ts",
1359
+ default: "./dist/index.js"
1360
+ },
1361
+ require: {
1362
+ types: "./dist/index.d.cts",
1363
+ default: "./dist/index.cjs"
1364
+ }
1365
+ }
1366
+ },
1367
+ files: [
1368
+ "dist"
1369
+ ],
1370
+ dependencies: {
1371
+ "@apps-in-toss/types": "workspace:*"
1372
+ },
1373
+ devDependencies: {
1374
+ "@apps-in-toss/bridge-core": "workspace:*",
1375
+ "@apps-in-toss/native-modules": "workspace:^",
1376
+ "@swc/core": "^1.12.7",
1377
+ picocolors: "^1.1.1",
1378
+ "ts-morph": "^26.0.0",
1379
+ tsup: "^8.5.0",
1380
+ typescript: "5.8.3",
1381
+ vitest: "^3.1.2"
1382
+ }
1383
+ };
1384
+
1385
+ // src/toss-ad/sdkVersion.ts
1386
+ var SDK_VERSION = package_default.version;
1387
+
1388
+ // src/toss-ad/trackingContext.ts
1389
+ function nonEmptyString(value) {
1390
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1391
+ }
1392
+ function extractAdTrackingContext(response) {
1393
+ const mediation = response?.ext?.mediation;
1394
+ return {
1395
+ mediationId: nonEmptyString(mediation?.mediationId),
1396
+ eventContextToken: nonEmptyString(response?.eventContextToken),
1397
+ resultEndpoint: nonEmptyString(mediation?.endpoint?.result),
1398
+ errorEndpoint: nonEmptyString(mediation?.endpoint?.error),
1399
+ adUnitId: nonEmptyString(mediation?.admob?.adUnitId),
1400
+ placementId: typeof mediation?.admob?.placementId === "number" ? mediation.admob.placementId : void 0
1401
+ };
1402
+ }
1403
+
1109
1404
  // src/toss-ad/index.ts
1110
1405
  var fetchTossAd = Object.assign(createEventBridge("fetchTossAd"), {
1111
1406
  isSupported: createConstantBridge("fetchTossAd_isSupported")
1112
1407
  });
1113
- var tossAdEventLog = createAsyncBridge("tossAdEventLog");
1114
- var SUPPORTED_STYLE_IDS = /* @__PURE__ */ new Set(["1", "2"]);
1408
+ var fetchAppsInTossAd = Object.assign(createEventBridge("fetchAppsInTossAd"), {
1409
+ isSupported: createConstantBridge("fetchAppsInTossAd_isSupported")
1410
+ });
1411
+ var getPlatformOS = createConstantBridge("getPlatformOS");
1412
+ var getTossAppVersion = createConstantBridge("getTossAppVersion");
1413
+ var WEB_SDK_ID = "108";
1115
1414
  var INVALID_AD_GROUP_ID_ERROR_MESSAGE = "\uC798\uBABB\uB41C \uC694\uCCAD\uC774\uC5D0\uC694. \uD544\uC694\uD55C \uAC12\uC774 \uBE44\uC5B4 \uC788\uC5B4\uC694.";
1116
1415
  function normalizeAdGroupId(adGroupId) {
1117
1416
  return adGroupId.trim();
@@ -1148,62 +1447,56 @@ function fetchTossAdPromise(options) {
1148
1447
  });
1149
1448
  });
1150
1449
  }
1151
- function normalizeAdResponse(adResponse) {
1152
- const ads = Array.isArray(adResponse.ads) ? adResponse.ads.filter((ad) => SUPPORTED_STYLE_IDS.has(String(ad.styleId))) : [];
1153
- return {
1154
- requestId: adResponse.requestId ?? "",
1155
- status: adResponse.status ?? "OK",
1156
- ads,
1157
- ext: adResponse.ext
1158
- };
1450
+ function getSubBundle() {
1451
+ return globalThis.__granite?.app?.name ?? "";
1159
1452
  }
1160
- function normalizeApiResponse(raw) {
1161
- if (isApiResponse(raw)) {
1162
- if (raw.resultType !== "SUCCESS") {
1163
- return raw;
1164
- }
1165
- if (!raw.success) {
1166
- return {
1167
- resultType: "FAIL",
1168
- error: { reason: "fetchTossAd returned SUCCESS without payload" }
1169
- };
1453
+ function fetchAppsInTossAdPromise(spaceUnitId) {
1454
+ const platformOS = getPlatformOS();
1455
+ const tossAppVersion = getTossAppVersion();
1456
+ const body = {
1457
+ specVersion: APPS_IN_TOSS_AD_SPEC_VERSION,
1458
+ platform: "WEB",
1459
+ spaceUnitId,
1460
+ sdkVersion: SDK_VERSION,
1461
+ availableStyleIds: AVAILABLE_STYLE_IDS,
1462
+ // device는 매크로키만 보내고 native가 실제 device로 치환한다.
1463
+ device: FETCH_APPS_IN_TOSS_AD_DEVICE_MACRO,
1464
+ app: {
1465
+ version: tossAppVersion,
1466
+ bundle: resolveTossAppBundleId(platformOS),
1467
+ subBundle: getSubBundle()
1170
1468
  }
1171
- return { ...raw, success: normalizeAdResponse(raw.success) };
1172
- }
1173
- if (isAdResponse(raw)) {
1174
- return {
1175
- resultType: "SUCCESS",
1176
- success: normalizeAdResponse(raw)
1177
- };
1178
- }
1179
- return { resultType: "FAIL", error: { reason: "Invalid response from fetchTossAd" } };
1180
- }
1181
- function isApiResponse(payload) {
1182
- return Boolean(payload && typeof payload === "object" && "resultType" in payload);
1183
- }
1184
- function isAdResponse(payload) {
1185
- return Boolean(payload && typeof payload === "object" && "ads" in payload);
1469
+ };
1470
+ return new Promise((resolve, reject) => {
1471
+ fetchAppsInTossAd({ options: body, onEvent: resolve, onError: reject });
1472
+ });
1186
1473
  }
1187
1474
  function createCustomAdFetcher() {
1188
1475
  return async (_endpoint, request) => {
1189
1476
  const spaceUnitId = normalizeAdGroupId(request.spaceUnitId);
1190
1477
  if (spaceUnitId.length === 0) {
1478
+ trackBannerError({
1479
+ adGroupId: spaceUnitId,
1480
+ errorCode: ERROR_CODES.INVALID_SPACE,
1481
+ errorMessage: INVALID_AD_GROUP_ID_ERROR_MESSAGE
1482
+ });
1191
1483
  return createInvalidAdGroupIdResponse();
1192
1484
  }
1193
1485
  try {
1194
- const raw = await fetchTossAdPromise({
1486
+ const raw = fetchAppsInTossAd.isSupported() ? await fetchAppsInTossAdPromise(spaceUnitId) : await fetchTossAdPromise({
1195
1487
  adGroupId: spaceUnitId,
1196
- sdkId: "108",
1197
- availableStyleIds: ["1", "2"]
1488
+ sdkId: WEB_SDK_ID,
1489
+ availableStyleIds: AVAILABLE_STYLE_IDS
1198
1490
  });
1199
- return normalizeApiResponse(raw);
1491
+ const response = normalizeApiResponse(raw);
1492
+ const trackingContext = extractAdTrackingContext(response.success);
1493
+ trackResponseError(response, spaceUnitId, trackingContext);
1494
+ void fireResultTracking(response, spaceUnitId, trackingContext);
1495
+ return response;
1200
1496
  } catch (error) {
1201
- return {
1202
- resultType: "FAIL",
1203
- error: {
1204
- reason: error instanceof Error ? error.message : "Unknown fetchTossAd error"
1205
- }
1206
- };
1497
+ const reason = error instanceof Error ? error.message : "Unknown fetchTossAd error";
1498
+ trackBannerError({ adGroupId: spaceUnitId, errorCode: ERROR_CODES.NETWORK_ERROR, errorMessage: reason });
1499
+ return { resultType: "FAIL", error: { reason } };
1207
1500
  }
1208
1501
  };
1209
1502
  }
@@ -1229,7 +1522,12 @@ function initialize(options) {
1229
1522
  return;
1230
1523
  }
1231
1524
  const customAdFetcher = createCustomAdFetcher();
1232
- const config = { environment: "live", customAdFetcher, opener: openUrlOpener };
1525
+ const config = {
1526
+ environment: "live",
1527
+ customAdFetcher,
1528
+ opener: openUrlOpener,
1529
+ onTrackingRequest: handleTrackingRequest
1530
+ };
1233
1531
  sdk.init(config);
1234
1532
  });
1235
1533
  pendingLoad2 = initPromise;
@@ -1406,7 +1704,7 @@ function attachBanner(adGroupId, target, options = {}) {
1406
1704
  autoLoad: true,
1407
1705
  theme: theme === "auto" ? void 0 : theme,
1408
1706
  renderPadding: (styleId) => {
1409
- if (styleId === "1") {
1707
+ if (styleId === LIST_BANNER_STYLE_ID) {
1410
1708
  return DEFAULT_ATTACH_PADDING_BANNER;
1411
1709
  }
1412
1710
  return DEFAULT_ATTACH_PADDING_NATIVE_IMAGE;
@@ -1479,6 +1777,8 @@ function wrapCallbacks(adGroupId, callbacks) {
1479
1777
  });
1480
1778
  callbacks.onAdImpression?.(mapEvent(payload));
1481
1779
  },
1780
+ // 에러 트래킹은 응답을 소유한 createCustomAdFetcher가 담당한다. 콜백은 publisher 전달만.
1781
+ // 응답 수신 후 에러의 mediationId는 SDK(ads-sdk/web)가 error에 실어주므로 그대로 전달한다.
1482
1782
  onAdFailedToRender: (payload) => callbacks.onAdFailedToRender?.({
1483
1783
  ...mapEvent(payload),
1484
1784
  error: payload?.error ?? { code: 0, message: "UNKNOWN" }
package/dist/index.d.cts CHANGED
@@ -885,6 +885,22 @@ interface RuntimeInfo {
885
885
  safeAreaInsets?: SafeAreaInsets;
886
886
  }
887
887
  type AdOpener = (url: string, target?: string, features?: string) => void;
888
+ /** SDK가 `onTrackingRequest`로 위임하는 트래킹 요청. ads-sdk/web의 동형 계약. */
889
+ interface TrackingRequestBase {
890
+ adGroupId: string;
891
+ requestId?: string;
892
+ creativeId?: string;
893
+ mediationId?: string;
894
+ eventContextToken?: string;
895
+ errorEndpoint?: string;
896
+ }
897
+ interface EventTrackingRequest extends TrackingRequestBase {
898
+ type: 'EVENT_TRACKING';
899
+ eventName: string;
900
+ urls: string[];
901
+ eventPayload: string;
902
+ }
903
+ type TrackingRequest = EventTrackingRequest;
888
904
  interface AdConfig {
889
905
  environment: Environment;
890
906
  apiEndpoint?: string;
@@ -893,6 +909,11 @@ interface AdConfig {
893
909
  runtimeInfo?: RuntimeInfo;
894
910
  opener?: AdOpener;
895
911
  customAdFetcher?: CustomAdFetcher;
912
+ /**
913
+ * 트래킹 전송 위임(SDK 3.1.0+). 지정 시 SDK는 직접 전송하지 않고 이 콜백에 위임한다.
914
+ * fire-and-forget이며 실패 관측은 사용측 책임. 현재 위임 대상은 EVENT_TRACKING이다.
915
+ */
916
+ onTrackingRequest?: (request: TrackingRequest) => void | Promise<void>;
896
917
  }
897
918
  interface AdRequest {
898
919
  spaceUnitId: string;
@@ -907,6 +928,7 @@ interface AdRequest {
907
928
  type AdResponseStatus = 'OK' | 'NO_AD' | 'BLOCKED' | 'ERROR' | 'TIMEOUT' | 'INVALID_SPACE' | 'INVALID_REQUEST' | 'LIMITED_AD' | 'CONSENT_REQUIRED' | 'TEST_MODE';
908
929
  interface AdResponse {
909
930
  requestId?: string;
931
+ eventContextToken?: string;
910
932
  status?: AdResponseStatus;
911
933
  ads: Ad[];
912
934
  ext?: AdResponseExt;
@@ -919,6 +941,19 @@ interface Ad {
919
941
  eventPayload?: string;
920
942
  requestId?: string;
921
943
  }
944
+ interface TossAdsMediation {
945
+ mediationId: string;
946
+ priority: Array<'TOSS' | 'ADMOB'>;
947
+ admob: {
948
+ adUnitId: string;
949
+ placementId: number;
950
+ } | null;
951
+ endpoint?: {
952
+ result?: string;
953
+ exposure?: string;
954
+ error?: string;
955
+ };
956
+ }
922
957
  interface AdResponseExt {
923
958
  skippableOffsetSeconds?: number;
924
959
  reward?: {
@@ -927,6 +962,7 @@ interface AdResponseExt {
927
962
  };
928
963
  isAdBadgeEnabled?: boolean;
929
964
  refetchSeconds?: number | null;
965
+ mediation?: TossAdsMediation;
930
966
  }
931
967
  interface ApiResponse<T> {
932
968
  resultType: 'SUCCESS' | 'HTTP_TIMEOUT' | 'NETWORK_ERROR' | 'EXECUTION_FAIL' | 'INTERRUPTED' | 'INTERNAL_ERROR' | 'FAIL';
@@ -958,10 +994,12 @@ interface BannerSlotErrorPayload {
958
994
  slotId: string;
959
995
  adGroupId: string;
960
996
  adMetadata: Record<string, never>;
997
+ /** `mediationId`는 `/api-web/v3/benefit-ads/display-ad/ads` 응답 수신 후 발생한 에러에만 포함된다. */
961
998
  error: {
962
999
  code: number;
963
1000
  message: string;
964
1001
  domain?: string;
1002
+ mediationId?: string;
965
1003
  };
966
1004
  }
967
1005
  interface BannerSlotCallbacks {
package/dist/index.d.ts CHANGED
@@ -885,6 +885,22 @@ interface RuntimeInfo {
885
885
  safeAreaInsets?: SafeAreaInsets;
886
886
  }
887
887
  type AdOpener = (url: string, target?: string, features?: string) => void;
888
+ /** SDK가 `onTrackingRequest`로 위임하는 트래킹 요청. ads-sdk/web의 동형 계약. */
889
+ interface TrackingRequestBase {
890
+ adGroupId: string;
891
+ requestId?: string;
892
+ creativeId?: string;
893
+ mediationId?: string;
894
+ eventContextToken?: string;
895
+ errorEndpoint?: string;
896
+ }
897
+ interface EventTrackingRequest extends TrackingRequestBase {
898
+ type: 'EVENT_TRACKING';
899
+ eventName: string;
900
+ urls: string[];
901
+ eventPayload: string;
902
+ }
903
+ type TrackingRequest = EventTrackingRequest;
888
904
  interface AdConfig {
889
905
  environment: Environment;
890
906
  apiEndpoint?: string;
@@ -893,6 +909,11 @@ interface AdConfig {
893
909
  runtimeInfo?: RuntimeInfo;
894
910
  opener?: AdOpener;
895
911
  customAdFetcher?: CustomAdFetcher;
912
+ /**
913
+ * 트래킹 전송 위임(SDK 3.1.0+). 지정 시 SDK는 직접 전송하지 않고 이 콜백에 위임한다.
914
+ * fire-and-forget이며 실패 관측은 사용측 책임. 현재 위임 대상은 EVENT_TRACKING이다.
915
+ */
916
+ onTrackingRequest?: (request: TrackingRequest) => void | Promise<void>;
896
917
  }
897
918
  interface AdRequest {
898
919
  spaceUnitId: string;
@@ -907,6 +928,7 @@ interface AdRequest {
907
928
  type AdResponseStatus = 'OK' | 'NO_AD' | 'BLOCKED' | 'ERROR' | 'TIMEOUT' | 'INVALID_SPACE' | 'INVALID_REQUEST' | 'LIMITED_AD' | 'CONSENT_REQUIRED' | 'TEST_MODE';
908
929
  interface AdResponse {
909
930
  requestId?: string;
931
+ eventContextToken?: string;
910
932
  status?: AdResponseStatus;
911
933
  ads: Ad[];
912
934
  ext?: AdResponseExt;
@@ -919,6 +941,19 @@ interface Ad {
919
941
  eventPayload?: string;
920
942
  requestId?: string;
921
943
  }
944
+ interface TossAdsMediation {
945
+ mediationId: string;
946
+ priority: Array<'TOSS' | 'ADMOB'>;
947
+ admob: {
948
+ adUnitId: string;
949
+ placementId: number;
950
+ } | null;
951
+ endpoint?: {
952
+ result?: string;
953
+ exposure?: string;
954
+ error?: string;
955
+ };
956
+ }
922
957
  interface AdResponseExt {
923
958
  skippableOffsetSeconds?: number;
924
959
  reward?: {
@@ -927,6 +962,7 @@ interface AdResponseExt {
927
962
  };
928
963
  isAdBadgeEnabled?: boolean;
929
964
  refetchSeconds?: number | null;
965
+ mediation?: TossAdsMediation;
930
966
  }
931
967
  interface ApiResponse<T> {
932
968
  resultType: 'SUCCESS' | 'HTTP_TIMEOUT' | 'NETWORK_ERROR' | 'EXECUTION_FAIL' | 'INTERRUPTED' | 'INTERNAL_ERROR' | 'FAIL';
@@ -958,10 +994,12 @@ interface BannerSlotErrorPayload {
958
994
  slotId: string;
959
995
  adGroupId: string;
960
996
  adMetadata: Record<string, never>;
997
+ /** `mediationId`는 `/api-web/v3/benefit-ads/display-ad/ads` 응답 수신 후 발생한 에러에만 포함된다. */
961
998
  error: {
962
999
  code: number;
963
1000
  message: string;
964
1001
  domain?: string;
1002
+ mediationId?: string;
965
1003
  };
966
1004
  }
967
1005
  interface BannerSlotCallbacks {
package/dist/index.js CHANGED
@@ -986,6 +986,197 @@ function resetAttachedBannerRegistry() {
986
986
  attachedBannerRegistry = /* @__PURE__ */ new WeakMap();
987
987
  }
988
988
 
989
+ // src/toss-ad/constants.ts
990
+ var ERROR_CODES = {
991
+ NETWORK_ERROR: 1001,
992
+ INVALID_REQUEST: 1002,
993
+ NO_AD: 1003,
994
+ ERROR: 1004,
995
+ INVALID_SPACE: 1005,
996
+ SDK_NOT_INITIALIZED: 1007,
997
+ HTTP_TIMEOUT: 1008,
998
+ EXECUTION_FAIL: 1009,
999
+ INTERRUPTED: 1010,
1000
+ FAIL: 1011,
1001
+ BLOCKED: 1012,
1002
+ TIMEOUT: 1013,
1003
+ LIMITED_AD: 1014,
1004
+ CONSENT_REQUIRED: 1015,
1005
+ TEST_MODE: 1016,
1006
+ INTERNAL_ERROR: 1017
1007
+ };
1008
+ var NON_ERROR_STATUSES = /* @__PURE__ */ new Set(["OK", "TEST_MODE", "NO_AD"]);
1009
+ var API_RESULT_ERROR_CODE_MAP = {
1010
+ HTTP_TIMEOUT: ERROR_CODES.HTTP_TIMEOUT,
1011
+ NETWORK_ERROR: ERROR_CODES.NETWORK_ERROR,
1012
+ EXECUTION_FAIL: ERROR_CODES.EXECUTION_FAIL,
1013
+ INTERRUPTED: ERROR_CODES.INTERRUPTED,
1014
+ INTERNAL_ERROR: ERROR_CODES.ERROR,
1015
+ FAIL: ERROR_CODES.FAIL
1016
+ };
1017
+ var AD_STATUS_ERROR_CODE_MAP = {
1018
+ BLOCKED: ERROR_CODES.BLOCKED,
1019
+ ERROR: ERROR_CODES.ERROR,
1020
+ TIMEOUT: ERROR_CODES.TIMEOUT,
1021
+ INVALID_SPACE: ERROR_CODES.INVALID_SPACE,
1022
+ INVALID_REQUEST: ERROR_CODES.INVALID_REQUEST,
1023
+ LIMITED_AD: ERROR_CODES.LIMITED_AD,
1024
+ CONSENT_REQUIRED: ERROR_CODES.CONSENT_REQUIRED
1025
+ };
1026
+
1027
+ // src/toss-ad/tossAdEventLog.ts
1028
+ var tossAdEventLog = createAsyncBridge("tossAdEventLog");
1029
+
1030
+ // src/toss-ad/errorTracking.ts
1031
+ var BANNER_ERROR_LOG_NAME = "display_ads_all::error__banner";
1032
+ function logBannerErrorToAppLog(info) {
1033
+ void tossAdEventLog({
1034
+ log_name: BANNER_ERROR_LOG_NAME,
1035
+ log_type: "debug",
1036
+ params: {
1037
+ error_code: info.errorCode,
1038
+ error_message: info.errorMessage,
1039
+ ad_group_id: info.adGroupId,
1040
+ mediation_id: info.mediationId,
1041
+ event_context_token: info.eventContextToken
1042
+ }
1043
+ }).catch(() => {
1044
+ });
1045
+ }
1046
+ function trackBannerError(info) {
1047
+ if (info.endpointError) {
1048
+ void fetch(info.endpointError, {
1049
+ method: "POST",
1050
+ headers: { "Content-Type": "application/json" },
1051
+ body: JSON.stringify({
1052
+ eventContextToken: info.eventContextToken,
1053
+ mediationId: info.mediationId,
1054
+ errorCode: info.errorCode,
1055
+ errorMessage: info.errorMessage
1056
+ })
1057
+ // POST 실패(CORS·네트워크) → 앱로그 fallback (endpoint와 같은 채널이라 함께 막히는 경우 대비).
1058
+ }).catch(() => logBannerErrorToAppLog(info));
1059
+ return;
1060
+ }
1061
+ logBannerErrorToAppLog(info);
1062
+ }
1063
+ function trackResponseError(response, adGroupId, context) {
1064
+ if (response.resultType !== "SUCCESS") {
1065
+ trackBannerError({
1066
+ adGroupId,
1067
+ errorCode: API_RESULT_ERROR_CODE_MAP[response.resultType] ?? ERROR_CODES.INTERNAL_ERROR,
1068
+ errorMessage: response.error?.reason ?? response.resultType
1069
+ });
1070
+ return;
1071
+ }
1072
+ const success = response.success;
1073
+ if (!success || NON_ERROR_STATUSES.has(String(success.status))) {
1074
+ return;
1075
+ }
1076
+ trackBannerError({
1077
+ adGroupId,
1078
+ errorCode: AD_STATUS_ERROR_CODE_MAP[String(success.status)] ?? ERROR_CODES.INTERNAL_ERROR,
1079
+ mediationId: context.mediationId,
1080
+ eventContextToken: context.eventContextToken,
1081
+ errorMessage: `Ad response status: ${success.status}`,
1082
+ endpointError: context.errorEndpoint
1083
+ });
1084
+ }
1085
+
1086
+ // src/toss-ad/eventTracking.ts
1087
+ async function handleTrackingRequest(request) {
1088
+ if (request.type === "EVENT_TRACKING") {
1089
+ await sendEventTracking(request);
1090
+ }
1091
+ }
1092
+ async function sendEventTracking(request) {
1093
+ if (request.urls.length === 0) {
1094
+ return;
1095
+ }
1096
+ const body = JSON.stringify({ type: request.eventName, data: request.eventPayload });
1097
+ const headers = { "Content-Type": "application/json" };
1098
+ if (request.requestId) {
1099
+ headers["X-Toss-RequestId"] = request.requestId;
1100
+ }
1101
+ const settled = await Promise.allSettled(
1102
+ request.urls.map(
1103
+ (url) => fetch(url, { method: "POST", headers, body }).then((response) => {
1104
+ if (!response.ok) {
1105
+ throw new Error(`HTTP ${response.status}`);
1106
+ }
1107
+ })
1108
+ )
1109
+ );
1110
+ const failedCount = settled.filter((result) => result.status === "rejected").length;
1111
+ if (failedCount === 0) {
1112
+ return;
1113
+ }
1114
+ trackBannerError({
1115
+ adGroupId: request.adGroupId,
1116
+ errorCode: ERROR_CODES.NETWORK_ERROR,
1117
+ errorMessage: `EVENT_TRACKING failed: ${request.eventName} (${failedCount}/${request.urls.length})`,
1118
+ mediationId: request.mediationId || void 0,
1119
+ eventContextToken: request.eventContextToken || void 0,
1120
+ endpointError: request.errorEndpoint
1121
+ });
1122
+ }
1123
+
1124
+ // src/toss-ad/fetchAppsInTossAdSchema.ts
1125
+ var APPS_IN_TOSS_AD_SPEC_VERSION = "1.4.1";
1126
+ var FETCH_APPS_IN_TOSS_AD_DEVICE_MACRO = "{{device}}";
1127
+ var TOSS_APP_BUNDLE_IDS = {
1128
+ ios: "com.vivarepublica.cash",
1129
+ android: "viva.republica.toss"
1130
+ };
1131
+ function resolveTossAppBundleId(os) {
1132
+ return TOSS_APP_BUNDLE_IDS[os];
1133
+ }
1134
+
1135
+ // src/toss-ad/normalize.ts
1136
+ var LIST_BANNER_STYLE_ID = "1";
1137
+ var NATIVE_IMAGE_STYLE_ID = "2";
1138
+ var AVAILABLE_STYLE_IDS = [LIST_BANNER_STYLE_ID, NATIVE_IMAGE_STYLE_ID];
1139
+ var SUPPORTED_STYLE_IDS = new Set(AVAILABLE_STYLE_IDS);
1140
+ function normalizeAdResponse(adResponse) {
1141
+ const ads = Array.isArray(adResponse.ads) ? adResponse.ads.filter((ad) => SUPPORTED_STYLE_IDS.has(String(ad.styleId))) : [];
1142
+ return {
1143
+ requestId: adResponse.requestId ?? "",
1144
+ // SDK가 event 트래킹 위임(onTrackingRequest) 시 응답 top-level에서 읽어 실어 보낸다.
1145
+ // 떼어내면 SDK가 토큰을 못 보고, event 트래킹 실패 시 endpoint.error에서 토큰이 빠진다.
1146
+ eventContextToken: adResponse.eventContextToken,
1147
+ status: adResponse.status ?? "OK",
1148
+ ads,
1149
+ ext: adResponse.ext
1150
+ };
1151
+ }
1152
+ function normalizeApiResponse(raw) {
1153
+ if (isApiResponse(raw)) {
1154
+ if (raw.resultType !== "SUCCESS") {
1155
+ return raw;
1156
+ }
1157
+ if (!raw.success) {
1158
+ return {
1159
+ resultType: "FAIL",
1160
+ error: { reason: "fetchTossAd returned SUCCESS without payload" }
1161
+ };
1162
+ }
1163
+ return { ...raw, success: normalizeAdResponse(raw.success) };
1164
+ }
1165
+ if (isAdResponse(raw)) {
1166
+ return {
1167
+ resultType: "SUCCESS",
1168
+ success: normalizeAdResponse(raw)
1169
+ };
1170
+ }
1171
+ return { resultType: "FAIL", error: { reason: "Invalid response from fetchTossAd" } };
1172
+ }
1173
+ function isApiResponse(payload) {
1174
+ return Boolean(payload && typeof payload === "object" && "resultType" in payload);
1175
+ }
1176
+ function isAdResponse(payload) {
1177
+ return Boolean(payload && typeof payload === "object" && "ads" in payload);
1178
+ }
1179
+
989
1180
  // src/toss-ad/opener.ts
990
1181
  var openURL = createAsyncBridge("openURL");
991
1182
  function openUrlOpener(url) {
@@ -1000,8 +1191,49 @@ function supertossWeb(uri) {
1000
1191
  return `supertoss://web?url=${encodeURIComponent(uri)}&external=true`;
1001
1192
  }
1002
1193
 
1194
+ // src/toss-ad/resultTracking.ts
1195
+ async function fireResultTracking(response, adGroupId, ctx) {
1196
+ const status = response.success?.status;
1197
+ if (response.resultType !== "SUCCESS" || status !== "OK" && status !== "TEST_MODE") {
1198
+ return;
1199
+ }
1200
+ if (ctx.resultEndpoint == null || ctx.mediationId == null) {
1201
+ return;
1202
+ }
1203
+ const body = {
1204
+ mediationId: ctx.mediationId,
1205
+ spaceUnitId: adGroupId,
1206
+ requestId: response.success?.requestId ?? "",
1207
+ eventContextToken: ctx.eventContextToken,
1208
+ winnerSource: "TOSS",
1209
+ content: null,
1210
+ adUnitId: ctx.adUnitId ?? null,
1211
+ placementId: ctx.placementId ?? null,
1212
+ tossFailed: null
1213
+ };
1214
+ try {
1215
+ const res = await fetch(ctx.resultEndpoint, {
1216
+ method: "POST",
1217
+ headers: { "Content-Type": "application/json" },
1218
+ body: JSON.stringify(body)
1219
+ });
1220
+ if (!res.ok) {
1221
+ throw new Error(`HTTP ${res.status}`);
1222
+ }
1223
+ } catch (error) {
1224
+ trackBannerError({
1225
+ adGroupId,
1226
+ errorCode: ERROR_CODES.NETWORK_ERROR,
1227
+ errorMessage: `RESULT_TRACKING failed: ${error instanceof Error ? error.message : String(error)}`,
1228
+ mediationId: ctx.mediationId,
1229
+ eventContextToken: ctx.eventContextToken,
1230
+ endpointError: ctx.errorEndpoint
1231
+ });
1232
+ }
1233
+ }
1234
+
1003
1235
  // src/toss-ad/scriptLoader.ts
1004
- var DEFAULT_SDK_URL = "https://static.toss.im/ads/sdk/toss-ads-space-kit-1.3.0.js";
1236
+ var DEFAULT_SDK_URL = "https://static.toss.im/ads/sdk/toss-ads-space-kit-3.1.0.js";
1005
1237
  var DEFAULT_TIMEOUT_MS = 15e3;
1006
1238
  var pendingLoad = null;
1007
1239
  function getAdsSdk() {
@@ -1057,12 +1289,79 @@ function loadAdsSdk() {
1057
1289
  return promise;
1058
1290
  }
1059
1291
 
1292
+ // package.json
1293
+ var package_default = {
1294
+ name: "@apps-in-toss/web-bridge",
1295
+ type: "module",
1296
+ version: "2.10.0",
1297
+ description: "Web Bridge for Apps In Toss",
1298
+ scripts: {
1299
+ typecheck: "tsc --noEmit",
1300
+ lint: "eslint .",
1301
+ build: "tsup"
1302
+ },
1303
+ main: "./dist/index.cjs",
1304
+ module: "./dist/index.js",
1305
+ types: "./dist/index.d.ts",
1306
+ exports: {
1307
+ ".": {
1308
+ import: {
1309
+ types: "./dist/index.d.ts",
1310
+ default: "./dist/index.js"
1311
+ },
1312
+ require: {
1313
+ types: "./dist/index.d.cts",
1314
+ default: "./dist/index.cjs"
1315
+ }
1316
+ }
1317
+ },
1318
+ files: [
1319
+ "dist"
1320
+ ],
1321
+ dependencies: {
1322
+ "@apps-in-toss/types": "workspace:*"
1323
+ },
1324
+ devDependencies: {
1325
+ "@apps-in-toss/bridge-core": "workspace:*",
1326
+ "@apps-in-toss/native-modules": "workspace:^",
1327
+ "@swc/core": "^1.12.7",
1328
+ picocolors: "^1.1.1",
1329
+ "ts-morph": "^26.0.0",
1330
+ tsup: "^8.5.0",
1331
+ typescript: "5.8.3",
1332
+ vitest: "^3.1.2"
1333
+ }
1334
+ };
1335
+
1336
+ // src/toss-ad/sdkVersion.ts
1337
+ var SDK_VERSION = package_default.version;
1338
+
1339
+ // src/toss-ad/trackingContext.ts
1340
+ function nonEmptyString(value) {
1341
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1342
+ }
1343
+ function extractAdTrackingContext(response) {
1344
+ const mediation = response?.ext?.mediation;
1345
+ return {
1346
+ mediationId: nonEmptyString(mediation?.mediationId),
1347
+ eventContextToken: nonEmptyString(response?.eventContextToken),
1348
+ resultEndpoint: nonEmptyString(mediation?.endpoint?.result),
1349
+ errorEndpoint: nonEmptyString(mediation?.endpoint?.error),
1350
+ adUnitId: nonEmptyString(mediation?.admob?.adUnitId),
1351
+ placementId: typeof mediation?.admob?.placementId === "number" ? mediation.admob.placementId : void 0
1352
+ };
1353
+ }
1354
+
1060
1355
  // src/toss-ad/index.ts
1061
1356
  var fetchTossAd = Object.assign(createEventBridge("fetchTossAd"), {
1062
1357
  isSupported: createConstantBridge("fetchTossAd_isSupported")
1063
1358
  });
1064
- var tossAdEventLog = createAsyncBridge("tossAdEventLog");
1065
- var SUPPORTED_STYLE_IDS = /* @__PURE__ */ new Set(["1", "2"]);
1359
+ var fetchAppsInTossAd = Object.assign(createEventBridge("fetchAppsInTossAd"), {
1360
+ isSupported: createConstantBridge("fetchAppsInTossAd_isSupported")
1361
+ });
1362
+ var getPlatformOS = createConstantBridge("getPlatformOS");
1363
+ var getTossAppVersion = createConstantBridge("getTossAppVersion");
1364
+ var WEB_SDK_ID = "108";
1066
1365
  var INVALID_AD_GROUP_ID_ERROR_MESSAGE = "\uC798\uBABB\uB41C \uC694\uCCAD\uC774\uC5D0\uC694. \uD544\uC694\uD55C \uAC12\uC774 \uBE44\uC5B4 \uC788\uC5B4\uC694.";
1067
1366
  function normalizeAdGroupId(adGroupId) {
1068
1367
  return adGroupId.trim();
@@ -1099,62 +1398,56 @@ function fetchTossAdPromise(options) {
1099
1398
  });
1100
1399
  });
1101
1400
  }
1102
- function normalizeAdResponse(adResponse) {
1103
- const ads = Array.isArray(adResponse.ads) ? adResponse.ads.filter((ad) => SUPPORTED_STYLE_IDS.has(String(ad.styleId))) : [];
1104
- return {
1105
- requestId: adResponse.requestId ?? "",
1106
- status: adResponse.status ?? "OK",
1107
- ads,
1108
- ext: adResponse.ext
1109
- };
1401
+ function getSubBundle() {
1402
+ return globalThis.__granite?.app?.name ?? "";
1110
1403
  }
1111
- function normalizeApiResponse(raw) {
1112
- if (isApiResponse(raw)) {
1113
- if (raw.resultType !== "SUCCESS") {
1114
- return raw;
1115
- }
1116
- if (!raw.success) {
1117
- return {
1118
- resultType: "FAIL",
1119
- error: { reason: "fetchTossAd returned SUCCESS without payload" }
1120
- };
1404
+ function fetchAppsInTossAdPromise(spaceUnitId) {
1405
+ const platformOS = getPlatformOS();
1406
+ const tossAppVersion = getTossAppVersion();
1407
+ const body = {
1408
+ specVersion: APPS_IN_TOSS_AD_SPEC_VERSION,
1409
+ platform: "WEB",
1410
+ spaceUnitId,
1411
+ sdkVersion: SDK_VERSION,
1412
+ availableStyleIds: AVAILABLE_STYLE_IDS,
1413
+ // device는 매크로키만 보내고 native가 실제 device로 치환한다.
1414
+ device: FETCH_APPS_IN_TOSS_AD_DEVICE_MACRO,
1415
+ app: {
1416
+ version: tossAppVersion,
1417
+ bundle: resolveTossAppBundleId(platformOS),
1418
+ subBundle: getSubBundle()
1121
1419
  }
1122
- return { ...raw, success: normalizeAdResponse(raw.success) };
1123
- }
1124
- if (isAdResponse(raw)) {
1125
- return {
1126
- resultType: "SUCCESS",
1127
- success: normalizeAdResponse(raw)
1128
- };
1129
- }
1130
- return { resultType: "FAIL", error: { reason: "Invalid response from fetchTossAd" } };
1131
- }
1132
- function isApiResponse(payload) {
1133
- return Boolean(payload && typeof payload === "object" && "resultType" in payload);
1134
- }
1135
- function isAdResponse(payload) {
1136
- return Boolean(payload && typeof payload === "object" && "ads" in payload);
1420
+ };
1421
+ return new Promise((resolve, reject) => {
1422
+ fetchAppsInTossAd({ options: body, onEvent: resolve, onError: reject });
1423
+ });
1137
1424
  }
1138
1425
  function createCustomAdFetcher() {
1139
1426
  return async (_endpoint, request) => {
1140
1427
  const spaceUnitId = normalizeAdGroupId(request.spaceUnitId);
1141
1428
  if (spaceUnitId.length === 0) {
1429
+ trackBannerError({
1430
+ adGroupId: spaceUnitId,
1431
+ errorCode: ERROR_CODES.INVALID_SPACE,
1432
+ errorMessage: INVALID_AD_GROUP_ID_ERROR_MESSAGE
1433
+ });
1142
1434
  return createInvalidAdGroupIdResponse();
1143
1435
  }
1144
1436
  try {
1145
- const raw = await fetchTossAdPromise({
1437
+ const raw = fetchAppsInTossAd.isSupported() ? await fetchAppsInTossAdPromise(spaceUnitId) : await fetchTossAdPromise({
1146
1438
  adGroupId: spaceUnitId,
1147
- sdkId: "108",
1148
- availableStyleIds: ["1", "2"]
1439
+ sdkId: WEB_SDK_ID,
1440
+ availableStyleIds: AVAILABLE_STYLE_IDS
1149
1441
  });
1150
- return normalizeApiResponse(raw);
1442
+ const response = normalizeApiResponse(raw);
1443
+ const trackingContext = extractAdTrackingContext(response.success);
1444
+ trackResponseError(response, spaceUnitId, trackingContext);
1445
+ void fireResultTracking(response, spaceUnitId, trackingContext);
1446
+ return response;
1151
1447
  } catch (error) {
1152
- return {
1153
- resultType: "FAIL",
1154
- error: {
1155
- reason: error instanceof Error ? error.message : "Unknown fetchTossAd error"
1156
- }
1157
- };
1448
+ const reason = error instanceof Error ? error.message : "Unknown fetchTossAd error";
1449
+ trackBannerError({ adGroupId: spaceUnitId, errorCode: ERROR_CODES.NETWORK_ERROR, errorMessage: reason });
1450
+ return { resultType: "FAIL", error: { reason } };
1158
1451
  }
1159
1452
  };
1160
1453
  }
@@ -1180,7 +1473,12 @@ function initialize(options) {
1180
1473
  return;
1181
1474
  }
1182
1475
  const customAdFetcher = createCustomAdFetcher();
1183
- const config = { environment: "live", customAdFetcher, opener: openUrlOpener };
1476
+ const config = {
1477
+ environment: "live",
1478
+ customAdFetcher,
1479
+ opener: openUrlOpener,
1480
+ onTrackingRequest: handleTrackingRequest
1481
+ };
1184
1482
  sdk.init(config);
1185
1483
  });
1186
1484
  pendingLoad2 = initPromise;
@@ -1357,7 +1655,7 @@ function attachBanner(adGroupId, target, options = {}) {
1357
1655
  autoLoad: true,
1358
1656
  theme: theme === "auto" ? void 0 : theme,
1359
1657
  renderPadding: (styleId) => {
1360
- if (styleId === "1") {
1658
+ if (styleId === LIST_BANNER_STYLE_ID) {
1361
1659
  return DEFAULT_ATTACH_PADDING_BANNER;
1362
1660
  }
1363
1661
  return DEFAULT_ATTACH_PADDING_NATIVE_IMAGE;
@@ -1430,6 +1728,8 @@ function wrapCallbacks(adGroupId, callbacks) {
1430
1728
  });
1431
1729
  callbacks.onAdImpression?.(mapEvent(payload));
1432
1730
  },
1731
+ // 에러 트래킹은 응답을 소유한 createCustomAdFetcher가 담당한다. 콜백은 publisher 전달만.
1732
+ // 응답 수신 후 에러의 mediationId는 SDK(ads-sdk/web)가 error에 실어주므로 그대로 전달한다.
1433
1733
  onAdFailedToRender: (payload) => callbacks.onAdFailedToRender?.({
1434
1734
  ...mapEvent(payload),
1435
1735
  error: payload?.error ?? { code: 0, message: "UNKNOWN" }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@apps-in-toss/web-bridge",
3
3
  "type": "module",
4
- "version": "2.9.3",
4
+ "version": "2.10.0",
5
5
  "description": "Web Bridge for Apps In Toss",
6
6
  "scripts": {
7
7
  "typecheck": "tsc --noEmit",
@@ -27,11 +27,11 @@
27
27
  "dist"
28
28
  ],
29
29
  "dependencies": {
30
- "@apps-in-toss/types": "2.9.3"
30
+ "@apps-in-toss/types": "2.10.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@apps-in-toss/bridge-core": "2.9.3",
34
- "@apps-in-toss/native-modules": "^2.9.3",
33
+ "@apps-in-toss/bridge-core": "2.10.0",
34
+ "@apps-in-toss/native-modules": "^2.10.0",
35
35
  "@swc/core": "^1.12.7",
36
36
  "picocolors": "^1.1.1",
37
37
  "ts-morph": "^26.0.0",