recur-tw 0.17.0 → 0.19.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
@@ -757,7 +757,7 @@ var init_payment_form_skeleton = __esmMin((() => {
757
757
  }));
758
758
 
759
759
  //#endregion
760
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/typeof.js
760
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/typeof.js
761
761
  function _typeof(o) {
762
762
  "@babel/helpers - typeof";
763
763
  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
@@ -769,7 +769,7 @@ function _typeof(o) {
769
769
  var init_typeof = __esmMin((() => {}));
770
770
 
771
771
  //#endregion
772
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPrimitive.js
772
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/toPrimitive.js
773
773
  function toPrimitive(t, r) {
774
774
  if ("object" != _typeof(t) || !t) return t;
775
775
  var e = t[Symbol.toPrimitive];
@@ -785,7 +785,7 @@ var init_toPrimitive = __esmMin((() => {
785
785
  }));
786
786
 
787
787
  //#endregion
788
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPropertyKey.js
788
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/toPropertyKey.js
789
789
  function toPropertyKey(t) {
790
790
  var i = toPrimitive(t, "string");
791
791
  return "symbol" == _typeof(i) ? i : i + "";
@@ -796,7 +796,7 @@ var init_toPropertyKey = __esmMin((() => {
796
796
  }));
797
797
 
798
798
  //#endregion
799
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/defineProperty.js
799
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/defineProperty.js
800
800
  function _defineProperty(e, r, t) {
801
801
  return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
802
802
  value: t,
@@ -1067,6 +1067,410 @@ var init_toast = __esmMin((() => {
1067
1067
  if (typeof window !== "undefined" && !customElements.get("recur-toast-container")) customElements.define("recur-toast-container", RecurToastContainer);
1068
1068
  }));
1069
1069
 
1070
+ //#endregion
1071
+ //#region src/einvoice.ts
1072
+ /**
1073
+ * 統一編號 checksum (post-2023 rule: weighted digit-sum divisible by 5, with
1074
+ * the 7-in-7th-digit alternate). Providers reject checksum-invalid UBNs at
1075
+ * issue time as a non-retryable failure, so the SDK catches this at entry.
1076
+ */
1077
+ function isValidTaiwanUbn(ubn) {
1078
+ if (!UBN_RE.test(ubn)) return false;
1079
+ const weights = [
1080
+ 1,
1081
+ 2,
1082
+ 1,
1083
+ 2,
1084
+ 1,
1085
+ 2,
1086
+ 4,
1087
+ 1
1088
+ ];
1089
+ let sum = 0;
1090
+ for (let i = 0; i < 8; i++) {
1091
+ const product = Number(ubn.charAt(i)) * (weights[i] ?? 0);
1092
+ sum += Math.floor(product / 10) + product % 10;
1093
+ }
1094
+ if (sum % 5 === 0) return true;
1095
+ return ubn.charAt(6) === "7" && (sum + 1) % 5 === 0;
1096
+ }
1097
+ /**
1098
+ * Validate buyer invoice preferences.
1099
+ *
1100
+ * @returns an error message, or `null` when the prefs are valid
1101
+ */
1102
+ function validateEinvoicePrefs(prefs) {
1103
+ if (typeof prefs !== "object" || prefs === null) return "Invalid einvoice prefs: expected an object like { type: \"personal\" }";
1104
+ switch (prefs.type) {
1105
+ case "personal": return null;
1106
+ case "mobile_barcode": return MOBILE_BARCODE_RE.test(prefs.carrierCode) ? null : "Invalid einvoice mobile barcode: must be \"/\" followed by 7 characters (0-9, A-Z, ., +, -)";
1107
+ case "ubn":
1108
+ if (!isValidTaiwanUbn(prefs.ubn)) return "Invalid einvoice UBN (統一編號): must be 8 digits with a valid checksum";
1109
+ if (!prefs.buyerName || !prefs.buyerName.trim() || prefs.buyerName.trim().length > 60) return "Invalid einvoice buyerName (公司抬頭): required, at most 60 characters";
1110
+ return null;
1111
+ case "donation": return DONATION_CODE_RE.test(prefs.donationCode) ? null : "Invalid einvoice donation code (愛心碼): must be 3-7 digits";
1112
+ default: return `Invalid einvoice type: ${String(prefs.type)}`;
1113
+ }
1114
+ }
1115
+ /** Throw when buyer invoice preferences are invalid (fail fast, before the API call). */
1116
+ function assertValidEinvoicePrefs(prefs) {
1117
+ const error = validateEinvoicePrefs(prefs);
1118
+ if (error) throw new Error(error);
1119
+ }
1120
+ var MOBILE_BARCODE_RE, UBN_RE, DONATION_CODE_RE;
1121
+ var init_einvoice = __esmMin((() => {
1122
+ MOBILE_BARCODE_RE = /^\/[0-9A-Z.+-]{7}$/;
1123
+ UBN_RE = /^\d{8}$/;
1124
+ DONATION_CODE_RE = /^\d{3,7}$/;
1125
+ }));
1126
+
1127
+ //#endregion
1128
+ //#region src/components/einvoice-section.ts
1129
+ /**
1130
+ * <recur-einvoice-section> — Taiwan e-invoice (電子發票) buyer preferences
1131
+ *
1132
+ * Rendered inside <recur-payment-form> when the organization has an active
1133
+ * e-invoice integration (einvoice-enabled attribute), mirroring the hosted
1134
+ * checkout's 發票資訊 section: four choices (個人 / 手機條碼 / 統編 / 捐贈)
1135
+ * with per-type fields, hints, and blur-triggered validation.
1136
+ *
1137
+ * Attributes:
1138
+ * - defaults: JSON-encoded EinvoicePrefs used as the initial selection
1139
+ *
1140
+ * API:
1141
+ * - getValidatedPrefs(): EinvoicePrefs | null — validates (surfacing inline
1142
+ * errors) and returns the prefs, or null when invalid
1143
+ * - einvoice-change CustomEvent (composed) fired on every state change with
1144
+ * detail.prefs (null while the current input is invalid)
1145
+ *
1146
+ * Self-contained Shadow DOM: no third-party SDK needs to reach these inputs
1147
+ * (unlike the PAYUNi card containers, which must live in Light DOM).
1148
+ */
1149
+ var einvoice_section_exports = /* @__PURE__ */ __exportAll({ RecurEinvoiceSection: () => RecurEinvoiceSection });
1150
+ var OPTIONS, styles, RecurEinvoiceSection;
1151
+ var init_einvoice_section = __esmMin((() => {
1152
+ init_einvoice();
1153
+ init_defineProperty();
1154
+ OPTIONS = [
1155
+ {
1156
+ value: "personal",
1157
+ label: "個人電子發票",
1158
+ caption: "發票將寄送至您的 Email"
1159
+ },
1160
+ {
1161
+ value: "mobile_barcode",
1162
+ label: "手機條碼載具",
1163
+ caption: "發票存入您的手機條碼"
1164
+ },
1165
+ {
1166
+ value: "ubn",
1167
+ label: "公司統編(三聯式)",
1168
+ caption: "開立含統編的三聯式發票"
1169
+ },
1170
+ {
1171
+ value: "donation",
1172
+ label: "捐贈發票",
1173
+ caption: "將發票捐贈給社福機構"
1174
+ }
1175
+ ];
1176
+ styles = `
1177
+ :host {
1178
+ display: block;
1179
+ font-family: inherit;
1180
+ color: var(--recur-text-color, #1f2937);
1181
+ }
1182
+ .einvoice-options { display: flex; flex-direction: column; gap: 8px; }
1183
+ .einvoice-option {
1184
+ display: flex; align-items: center; gap: 10px;
1185
+ padding: 10px 12px;
1186
+ border: 1px solid var(--recur-border-color, #e5e7eb);
1187
+ border-radius: var(--recur-border-radius, 8px);
1188
+ cursor: pointer;
1189
+ background: var(--recur-bg-color, #fff);
1190
+ transition: border-color 0.15s;
1191
+ text-align: left;
1192
+ width: 100%;
1193
+ font: inherit;
1194
+ }
1195
+ .einvoice-option[aria-checked="true"] {
1196
+ border-color: var(--recur-primary-color, #2563eb);
1197
+ box-shadow: 0 0 0 1px var(--recur-primary-color, #2563eb);
1198
+ }
1199
+ .einvoice-option .radio-dot {
1200
+ width: 16px; height: 16px; border-radius: 50%;
1201
+ border: 1.5px solid var(--recur-border-color, #d1d5db);
1202
+ flex-shrink: 0; position: relative;
1203
+ }
1204
+ .einvoice-option[aria-checked="true"] .radio-dot {
1205
+ border-color: var(--recur-primary-color, #2563eb);
1206
+ }
1207
+ .einvoice-option[aria-checked="true"] .radio-dot::after {
1208
+ content: ''; position: absolute; inset: 3px;
1209
+ border-radius: 50%; background: var(--recur-primary-color, #2563eb);
1210
+ }
1211
+ .option-label { font-size: 14px; font-weight: 500; }
1212
+ .option-caption { font-size: 12px; color: var(--recur-muted-color, #6b7280); }
1213
+ .einvoice-fields { margin-top: 10px; display: flex; flex-direction: column; gap: 10px; }
1214
+ .field label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 4px; }
1215
+ .field input {
1216
+ width: 100%; box-sizing: border-box;
1217
+ padding: 8px 10px; font-size: 14px; font-family: inherit;
1218
+ border: 1px solid var(--recur-border-color, #e5e7eb);
1219
+ border-radius: var(--recur-border-radius, 8px);
1220
+ background: var(--recur-bg-color, #fff);
1221
+ color: inherit;
1222
+ }
1223
+ .field input:focus {
1224
+ outline: none;
1225
+ border-color: var(--recur-primary-color, #2563eb);
1226
+ box-shadow: 0 0 0 1px var(--recur-primary-color, #2563eb);
1227
+ }
1228
+ .field input[aria-invalid="true"] { border-color: var(--recur-error-color, #dc2626); }
1229
+ .field .hint { font-size: 12px; color: var(--recur-muted-color, #6b7280); margin-top: 4px; }
1230
+ .field .error { font-size: 12px; color: var(--recur-error-color, #dc2626); margin-top: 4px; }
1231
+ `;
1232
+ RecurEinvoiceSection = class extends HTMLElement {
1233
+ constructor() {
1234
+ super();
1235
+ _defineProperty(this, "selectedType", "personal");
1236
+ _defineProperty(this, "values", {
1237
+ carrierCode: "",
1238
+ ubn: "",
1239
+ buyerName: "",
1240
+ donationCode: ""
1241
+ });
1242
+ _defineProperty(this, "touched", {
1243
+ carrierCode: false,
1244
+ ubn: false,
1245
+ buyerName: false,
1246
+ donationCode: false
1247
+ });
1248
+ this.attachShadow({ mode: "open" });
1249
+ }
1250
+ static get observedAttributes() {
1251
+ return ["defaults"];
1252
+ }
1253
+ connectedCallback() {
1254
+ this.applyDefaults(this.getAttribute("defaults"));
1255
+ this.render();
1256
+ }
1257
+ attributeChangedCallback(name, oldValue, newValue) {
1258
+ if (name === "defaults" && oldValue !== newValue) {
1259
+ this.applyDefaults(newValue);
1260
+ if (this.isConnected) this.render();
1261
+ }
1262
+ }
1263
+ applyDefaults(raw) {
1264
+ if (!raw) return;
1265
+ try {
1266
+ const prefs = JSON.parse(raw);
1267
+ this.selectedType = "personal";
1268
+ switch (prefs.type) {
1269
+ case "mobile_barcode":
1270
+ this.selectedType = "mobile_barcode";
1271
+ this.values.carrierCode = "carrierCode" in prefs && prefs.carrierCode || "";
1272
+ break;
1273
+ case "ubn":
1274
+ this.selectedType = "ubn";
1275
+ this.values.ubn = "ubn" in prefs && prefs.ubn || "";
1276
+ this.values.buyerName = "buyerName" in prefs && prefs.buyerName || "";
1277
+ break;
1278
+ case "donation":
1279
+ this.selectedType = "donation";
1280
+ this.values.donationCode = "donationCode" in prefs && prefs.donationCode || "";
1281
+ break;
1282
+ case "personal":
1283
+ this.selectedType = "personal";
1284
+ break;
1285
+ }
1286
+ } catch {}
1287
+ }
1288
+ buildPrefs() {
1289
+ switch (this.selectedType) {
1290
+ case "mobile_barcode": return {
1291
+ type: "mobile_barcode",
1292
+ carrierCode: this.values.carrierCode
1293
+ };
1294
+ case "ubn": return {
1295
+ type: "ubn",
1296
+ ubn: this.values.ubn,
1297
+ buyerName: this.values.buyerName.trim()
1298
+ };
1299
+ case "donation": return {
1300
+ type: "donation",
1301
+ donationCode: this.values.donationCode
1302
+ };
1303
+ default: return { type: "personal" };
1304
+ }
1305
+ }
1306
+ fieldError(field) {
1307
+ switch (field) {
1308
+ case "carrierCode":
1309
+ if (this.selectedType !== "mobile_barcode") return null;
1310
+ return MOBILE_BARCODE_RE.test(this.values.carrierCode) ? null : "手機條碼格式錯誤(/ 開頭共 8 碼)";
1311
+ case "ubn":
1312
+ if (this.selectedType !== "ubn") return null;
1313
+ if (!UBN_RE.test(this.values.ubn)) return "統一編號須為 8 位數字";
1314
+ return isValidTaiwanUbn(this.values.ubn) ? null : "統一編號檢查碼錯誤,請確認輸入是否正確";
1315
+ case "buyerName":
1316
+ if (this.selectedType !== "ubn") return null;
1317
+ return this.values.buyerName.trim() ? null : "請填寫公司抬頭";
1318
+ case "donationCode":
1319
+ if (this.selectedType !== "donation") return null;
1320
+ return DONATION_CODE_RE.test(this.values.donationCode) ? null : "愛心碼須為 3-7 位數字";
1321
+ }
1322
+ }
1323
+ isValid() {
1324
+ return [
1325
+ "carrierCode",
1326
+ "ubn",
1327
+ "buyerName",
1328
+ "donationCode"
1329
+ ].every((field) => this.fieldError(field) === null);
1330
+ }
1331
+ /**
1332
+ * Validate the current selection, surfacing inline errors.
1333
+ * @returns the prefs when valid, null otherwise
1334
+ */
1335
+ getValidatedPrefs() {
1336
+ this.touched = {
1337
+ carrierCode: true,
1338
+ ubn: true,
1339
+ buyerName: true,
1340
+ donationCode: true
1341
+ };
1342
+ this.render();
1343
+ return this.isValid() ? this.buildPrefs() : null;
1344
+ }
1345
+ emitChange() {
1346
+ this.dispatchEvent(new CustomEvent("einvoice-change", {
1347
+ detail: { prefs: this.isValid() ? this.buildPrefs() : null },
1348
+ bubbles: true,
1349
+ composed: true
1350
+ }));
1351
+ }
1352
+ selectType(type) {
1353
+ if (this.selectedType === type) return;
1354
+ this.selectedType = type;
1355
+ this.render();
1356
+ this.emitChange();
1357
+ }
1358
+ /** ARIA radio keyboard pattern: arrow keys move selection (with wrap). */
1359
+ onOptionKeydown(event, index) {
1360
+ const delta = event.key === "ArrowDown" || event.key === "ArrowRight" ? 1 : event.key === "ArrowUp" || event.key === "ArrowLeft" ? -1 : 0;
1361
+ if (delta === 0) return;
1362
+ event.preventDefault();
1363
+ const next = (index + delta + OPTIONS.length) % OPTIONS.length;
1364
+ this.selectType(OPTIONS[next].value);
1365
+ this.shadowRoot.querySelectorAll("[role=\"radio\"]")[next]?.focus();
1366
+ }
1367
+ onInput(field, event) {
1368
+ const input = event.target;
1369
+ let value = input.value;
1370
+ if (field === "carrierCode") {
1371
+ value = value.toUpperCase();
1372
+ input.value = value;
1373
+ }
1374
+ if (field === "ubn" || field === "donationCode") {
1375
+ value = value.replace(/\D/g, "");
1376
+ input.value = value;
1377
+ }
1378
+ this.values[field] = value;
1379
+ this.emitChange();
1380
+ }
1381
+ onBlur(field) {
1382
+ this.touched[field] = true;
1383
+ this.render();
1384
+ }
1385
+ renderField(opts) {
1386
+ const { field, label, placeholder, hint, maxLength, inputMode } = opts;
1387
+ const error = this.touched[field] ? this.fieldError(field) : null;
1388
+ return lit_html.html`
1389
+ <div class="field">
1390
+ <label for="einvoice-${field}">${label}</label>
1391
+ <input
1392
+ id="einvoice-${field}"
1393
+ type="text"
1394
+ .value=${this.values[field]}
1395
+ placeholder=${placeholder}
1396
+ maxlength=${maxLength ?? lit_html.nothing}
1397
+ inputmode=${inputMode ?? lit_html.nothing}
1398
+ aria-invalid=${error ? "true" : "false"}
1399
+ @input=${(e) => this.onInput(field, e)}
1400
+ @blur=${() => this.onBlur(field)}
1401
+ />
1402
+ ${error ? lit_html.html`<p class="error" role="alert">${error}</p>` : hint ? lit_html.html`<p class="hint">${hint}</p>` : lit_html.nothing}
1403
+ </div>
1404
+ `;
1405
+ }
1406
+ renderFields() {
1407
+ switch (this.selectedType) {
1408
+ case "mobile_barcode": return lit_html.html`<div class="einvoice-fields">
1409
+ ${this.renderField({
1410
+ field: "carrierCode",
1411
+ label: "手機條碼",
1412
+ placeholder: "/ABC1234",
1413
+ hint: "手機條碼共 8 碼,以 / 開頭",
1414
+ maxLength: 8
1415
+ })}
1416
+ </div>`;
1417
+ case "ubn": return lit_html.html`<div class="einvoice-fields">
1418
+ ${this.renderField({
1419
+ field: "ubn",
1420
+ label: "統一編號",
1421
+ placeholder: "12345675",
1422
+ maxLength: 8,
1423
+ inputMode: "numeric"
1424
+ })}
1425
+ ${this.renderField({
1426
+ field: "buyerName",
1427
+ label: "公司抬頭",
1428
+ placeholder: "公司名稱",
1429
+ maxLength: 60
1430
+ })}
1431
+ </div>`;
1432
+ case "donation": return lit_html.html`<div class="einvoice-fields">
1433
+ ${this.renderField({
1434
+ field: "donationCode",
1435
+ label: "愛心碼",
1436
+ placeholder: "25885",
1437
+ hint: "例如:25885(家扶基金會)、8957(創世基金會)",
1438
+ maxLength: 7,
1439
+ inputMode: "numeric"
1440
+ })}
1441
+ </div>`;
1442
+ default: return lit_html.nothing;
1443
+ }
1444
+ }
1445
+ render() {
1446
+ (0, lit_html.render)(lit_html.html`
1447
+ <style>${styles}</style>
1448
+ <div class="einvoice-options" role="radiogroup" aria-label="發票類型">
1449
+ ${OPTIONS.map((option, index) => lit_html.html`
1450
+ <button
1451
+ type="button"
1452
+ class="einvoice-option"
1453
+ role="radio"
1454
+ aria-checked=${this.selectedType === option.value ? "true" : "false"}
1455
+ tabindex=${this.selectedType === option.value ? "0" : "-1"}
1456
+ @click=${() => this.selectType(option.value)}
1457
+ @keydown=${(e) => this.onOptionKeydown(e, index)}
1458
+ >
1459
+ <span class="radio-dot"></span>
1460
+ <span>
1461
+ <span class="option-label">${option.label}</span><br />
1462
+ <span class="option-caption">${option.caption}</span>
1463
+ </span>
1464
+ </button>
1465
+ `)}
1466
+ </div>
1467
+ ${this.renderFields()}
1468
+ `, this.shadowRoot);
1469
+ }
1470
+ };
1471
+ if (typeof window !== "undefined" && !customElements.get("recur-einvoice-section")) customElements.define("recur-einvoice-section", RecurEinvoiceSection);
1472
+ }));
1473
+
1070
1474
  //#endregion
1071
1475
  //#region src/components/styles/payment-form-shadow.css?css-text
1072
1476
  var payment_form_shadow_default;
@@ -1078,7 +1482,7 @@ var init_payment_form_shadow = __esmMin((() => {
1078
1482
  //#region src/components/styles/payment-form-light.css?css-text
1079
1483
  var payment_form_light_default;
1080
1484
  var init_payment_form_light = __esmMin((() => {
1081
- payment_form_light_default = "/**\n * Payment Form - Light DOM Styles\n * These styles are applied to slotted content in the Light DOM\n * Using BEM naming convention with 'recur-' prefix for isolation\n */\n\n/* ============================================\n * Order Summary Styles\n * ============================================ */\n.recur-order-summary {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n padding: 16px;\n}\n\n.recur-order-summary__row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.recur-order-summary__row--total {\n margin-bottom: 0;\n padding-top: 12px;\n border-top: 1px solid #e2e8f0;\n}\n\n.recur-order-summary__label {\n font-size: 14px;\n color: #4a5568;\n}\n\n.recur-order-summary__value {\n font-size: 14px;\n line-height: 20px;\n font-weight: 500;\n color: #2d3748;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.recur-order-summary__total {\n font-size: 18px;\n line-height: 26px;\n font-weight: 700;\n color: #1a202c;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* ============================================\n * Coupon Section Styles\n * ============================================ */\n.recur-coupon {\n padding: 12px 0;\n border-top: 1px solid #e2e8f0;\n margin-top: 12px;\n min-height: 36px;\n display: flex;\n align-items: center;\n}\n\n.recur-coupon__trigger {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n height: 36px;\n background: none;\n border: none;\n padding: 0;\n font-size: 14px;\n line-height: 1;\n color: #718096;\n cursor: pointer;\n transition: color 0.2s;\n}\n\n.recur-coupon__trigger:hover {\n color: #4a5568;\n}\n\n.recur-coupon__trigger-icon {\n width: 16px;\n height: 16px;\n}\n\n.recur-coupon__input-row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__input-wrapper {\n position: relative;\n flex: 1;\n min-width: 0;\n}\n\n.recur-coupon__input {\n width: 100%;\n height: 36px;\n padding: 0 32px 0 12px;\n font-size: 14px;\n text-transform: uppercase;\n background-color: #ffffff;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n outline: none;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n.recur-coupon__input::placeholder {\n text-transform: none;\n}\n\n.recur-coupon__input:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n.recur-coupon__input:focus {\n border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.recur-coupon__clear {\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: color 0.2s;\n}\n\n.recur-coupon__clear:hover {\n color: #6b7280;\n}\n\n.recur-coupon__clear-icon {\n width: 16px;\n height: 16px;\n}\n\n.recur-coupon__btn {\n height: 36px;\n padding: 0 12px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.2s, opacity 0.2s;\n white-space: nowrap;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.recur-coupon__btn--apply {\n background: #18181b;\n color: white;\n border: none;\n min-width: 52px;\n}\n\n.recur-coupon__btn--apply .recur-coupon__spinner {\n width: 14px;\n height: 14px;\n}\n\n.recur-coupon__btn--apply:hover:not(:disabled) {\n background: #27272a;\n}\n\n.recur-coupon__btn--apply:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.recur-coupon__error {\n width: 100%;\n margin-top: 8px;\n font-size: 13px;\n color: #dc2626;\n}\n\n.recur-coupon__applied-wrapper {\n width: 100%;\n}\n\n.recur-coupon__description {\n font-size: 12px;\n color: #6b7280;\n margin: 4px 0 0 22px;\n}\n\n.recur-coupon__bonus {\n display: flex;\n align-items: center;\n gap: 6px;\n background: #dcfce7;\n border-radius: 6px;\n padding: 8px 12px;\n margin-top: 8px;\n font-size: 12px;\n color: #15803d;\n}\n\n.recur-coupon__applied {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__applied-info {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n flex: 1;\n}\n\n.recur-coupon__applied-icon {\n width: 16px;\n height: 16px;\n color: #dc2626;\n flex-shrink: 0;\n}\n\n.recur-coupon__applied-name {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n}\n\n.recur-coupon__applied-code {\n font-size: 14px;\n color: #9ca3af;\n white-space: nowrap;\n margin-right: 2px;\n}\n\n.recur-coupon__applied-amount {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n flex-shrink: 0;\n margin-left: auto;\n}\n\n.recur-coupon__remove {\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n transition: color 0.2s, background-color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.recur-coupon__remove:hover {\n color: #6b7280;\n background: rgba(0, 0, 0, 0.05);\n}\n\n.recur-coupon__remove-icon {\n width: 14px;\n height: 14px;\n}\n\n.recur-coupon__spinner {\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n animation: recur-spin 0.6s linear infinite;\n}\n\n/* Discount Row */\n.recur-order-summary__row--discount {\n color: #16a34a;\n}\n\n.recur-order-summary__row--discount .recur-order-summary__value {\n color: #16a34a;\n font-weight: 500;\n}\n\n/* ============================================\n * Customer Info Styles\n * ============================================ */\n.recur-info-display {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 6px;\n padding: 12px;\n margin-bottom: 8px;\n}\n\n.recur-info-row {\n margin-bottom: 12px;\n}\n\n.recur-info-row:last-child {\n margin-bottom: 0;\n}\n\n.recur-info-label {\n display: block;\n font-size: 12px;\n line-height: 16px;\n color: #718096;\n margin-bottom: 2px;\n}\n\n.recur-info-value {\n display: block;\n font-size: 14px;\n line-height: 20px;\n min-height: 20px;\n color: #2d3748;\n font-weight: 500;\n word-break: break-all;\n}\n\n.recur-form-group {\n margin-bottom: 16px;\n}\n\n.recur-form-label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n.recur-form-input {\n width: 100%;\n height: 36px;\n padding: 0 12px;\n font-size: 14px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n transition: border-color 0.2s;\n box-sizing: border-box;\n}\n\n.recur-form-input:focus {\n border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.recur-form-input:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n/* ============================================\n * Card Fields Styles\n * ============================================ */\n.recur-card-field {\n margin-bottom: 16px;\n}\n\n.recur-card-field-label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n.recur-payuni-iframe {\n height: 36px;\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n touch-action: manipulation;\n}\n\n.recur-card-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n column-gap: 16px;\n max-width: 100%;\n box-sizing: border-box;\n}\n\n.recur-card-field-container {\n position: relative;\n touch-action: manipulation;\n}\n\n.recur-card-skeleton {\n position: absolute;\n top: 0;\n left: 0;\n height: 36px;\n width: 100%;\n background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 50%, #f0f0f0 100%);\n background-size: 200% 100%;\n animation: recur-skeleton-loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n z-index: 1;\n pointer-events: none;\n}\n\n.recur-card-skeleton.hidden {\n display: none;\n}\n\n@keyframes recur-skeleton-loading {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n\n/* ============================================\n * Submit Button Styles\n * ============================================ */\n.recur-submit-button {\n width: 100%;\n padding: 14px 20px;\n font-size: 16px;\n font-weight: 500;\n color: #ffffff;\n background: #18181b;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n box-sizing: border-box;\n}\n\n.recur-submit-button:hover:not(:disabled) {\n background: #27272a;\n}\n\n.recur-submit-button:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.recur-submit-button.loading {\n cursor: wait;\n}\n\n.recur-loading-spinner {\n width: 16px;\n height: 16px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n animation: recur-spin 0.6s linear infinite;\n}\n\n@keyframes recur-spin {\n to { transform: rotate(360deg); }\n}\n\n/* ============================================\n * Skeleton Loading Styles\n * ============================================ */\n.recur-skeleton {\n display: inline-block;\n background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 50%, #f0f0f0 100%);\n background-size: 200% 100%;\n animation: recur-skeleton-loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n}\n\n/* ============================================\n * Test Mode Banner Styles\n * ============================================ */\n.recur-test-mode-banner {\n background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);\n border: 1px solid #f59e0b;\n border-radius: 8px;\n margin-bottom: 16px;\n overflow: hidden;\n}\n\n.recur-test-mode-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 10px 14px;\n cursor: pointer;\n user-select: none;\n transition: background 0.2s;\n}\n\n.recur-test-mode-header:hover {\n background: rgba(245, 158, 11, 0.1);\n}\n\n.recur-test-mode-badge {\n font-size: 13px;\n font-weight: 600;\n color: #92400e;\n}\n\n.recur-test-mode-toggle {\n font-size: 12px;\n color: #b45309;\n}\n\n.recur-test-cards {\n background: rgba(255, 255, 255, 0.7);\n padding: 12px 14px;\n border-top: 1px solid rgba(245, 158, 11, 0.3);\n}\n\n.recur-test-cards-info {\n font-size: 12px;\n color: #78350f;\n margin: 0 0 10px 0;\n}\n\n.recur-test-card {\n background: white;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n padding: 10px 12px;\n margin-bottom: 8px;\n}\n\n.recur-test-card:last-child {\n margin-bottom: 0;\n}\n\n.recur-test-card-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 4px;\n}\n\n.recur-test-card-number {\n font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;\n font-size: 14px;\n font-weight: 600;\n color: #1f2937;\n letter-spacing: 0.5px;\n}\n\n.recur-test-card-copy {\n font-size: 11px;\n font-weight: 500;\n color: #3b82f6;\n background: #eff6ff;\n border: 1px solid #bfdbfe;\n border-radius: 4px;\n padding: 3px 8px;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n.recur-test-card-copy:hover {\n background: #dbeafe;\n border-color: #93c5fd;\n}\n\n.recur-test-card-copy.copied {\n background: #dcfce7;\n border-color: #86efac;\n color: #16a34a;\n}\n\n.recur-test-card-details {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n font-size: 11px;\n color: #6b7280;\n}\n\n.recur-test-card-desc {\n color: #374151;\n font-weight: 500;\n}\n\n.recur-test-card-meta {\n color: #9ca3af;\n}\n";
1485
+ payment_form_light_default = "/**\n * Payment Form - Light DOM Styles\n * These styles are applied to slotted content in the Light DOM\n * Using BEM naming convention with 'recur-' prefix for isolation\n */\n\n/* ============================================\n * Order Summary Styles\n * ============================================ */\n.recur-order-summary {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n padding: 16px;\n}\n\n.recur-order-summary__row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.recur-order-summary__row--total {\n margin-bottom: 0;\n padding-top: 12px;\n border-top: 1px solid #e2e8f0;\n}\n\n.recur-order-summary__label {\n font-size: 14px;\n color: #4a5568;\n}\n\n.recur-order-summary__value {\n font-size: 14px;\n line-height: 20px;\n font-weight: 500;\n color: #2d3748;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.recur-order-summary__total {\n font-size: 18px;\n line-height: 26px;\n font-weight: 700;\n color: #1a202c;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* ============================================\n * Coupon Section Styles\n * ============================================ */\n.recur-coupon {\n padding: 12px 0;\n border-top: 1px solid #e2e8f0;\n margin-top: 12px;\n min-height: 36px;\n display: flex;\n align-items: center;\n}\n\n.recur-coupon__trigger {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n height: 36px;\n background: none;\n border: none;\n padding: 0;\n font-size: 14px;\n line-height: 1;\n color: #718096;\n cursor: pointer;\n transition: color 0.2s;\n}\n\n.recur-coupon__trigger:hover {\n color: #4a5568;\n}\n\n.recur-coupon__trigger-icon {\n width: 16px;\n height: 16px;\n}\n\n.recur-coupon__input-row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__input-wrapper {\n position: relative;\n flex: 1;\n min-width: 0;\n}\n\n.recur-coupon__input {\n width: 100%;\n height: 36px;\n padding: 0 32px 0 12px;\n font-size: 14px;\n text-transform: uppercase;\n background-color: #ffffff;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n outline: none;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n.recur-coupon__input::placeholder {\n text-transform: none;\n}\n\n.recur-coupon__input:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n.recur-coupon__input:focus {\n border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.recur-coupon__clear {\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: color 0.2s;\n}\n\n.recur-coupon__clear:hover {\n color: #6b7280;\n}\n\n.recur-coupon__clear-icon {\n width: 16px;\n height: 16px;\n}\n\n.recur-coupon__btn {\n height: 36px;\n padding: 0 12px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.2s, opacity 0.2s;\n white-space: nowrap;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.recur-coupon__btn--apply {\n background: #18181b;\n color: white;\n border: none;\n min-width: 52px;\n}\n\n.recur-coupon__btn--apply .recur-coupon__spinner {\n width: 14px;\n height: 14px;\n}\n\n.recur-coupon__btn--apply:hover:not(:disabled) {\n background: #27272a;\n}\n\n.recur-coupon__btn--apply:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.recur-coupon__error {\n width: 100%;\n margin-top: 8px;\n font-size: 13px;\n color: #dc2626;\n}\n\n.recur-coupon__applied-wrapper {\n width: 100%;\n}\n\n.recur-coupon__description {\n font-size: 12px;\n color: #6b7280;\n margin: 4px 0 0 22px;\n}\n\n.recur-coupon__bonus {\n display: flex;\n align-items: center;\n gap: 6px;\n background: #dcfce7;\n border-radius: 6px;\n padding: 8px 12px;\n margin-top: 8px;\n font-size: 12px;\n color: #15803d;\n}\n\n.recur-coupon__applied {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__applied-info {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n flex: 1;\n}\n\n.recur-coupon__applied-icon {\n width: 16px;\n height: 16px;\n color: #dc2626;\n flex-shrink: 0;\n}\n\n.recur-coupon__applied-name {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n}\n\n.recur-coupon__applied-code {\n font-size: 14px;\n color: #9ca3af;\n white-space: nowrap;\n margin-right: 2px;\n}\n\n.recur-coupon__applied-amount {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n flex-shrink: 0;\n margin-left: auto;\n}\n\n.recur-coupon__remove {\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n transition: color 0.2s, background-color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.recur-coupon__remove:hover {\n color: #6b7280;\n background: rgba(0, 0, 0, 0.05);\n}\n\n.recur-coupon__remove-icon {\n width: 14px;\n height: 14px;\n}\n\n.recur-coupon__spinner {\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n animation: recur-spin 0.6s linear infinite;\n}\n\n/* Discount Row */\n.recur-order-summary__row--discount {\n color: #16a34a;\n}\n\n.recur-order-summary__row--discount .recur-order-summary__value {\n color: #16a34a;\n font-weight: 500;\n}\n\n/* ============================================\n * Customer Info Styles\n * ============================================ */\n.recur-info-display {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 6px;\n padding: 12px;\n margin-bottom: 8px;\n}\n\n.recur-info-row {\n margin-bottom: 12px;\n}\n\n.recur-info-row:last-child {\n margin-bottom: 0;\n}\n\n.recur-info-label {\n display: block;\n font-size: 12px;\n line-height: 16px;\n color: #718096;\n margin-bottom: 2px;\n}\n\n.recur-info-value {\n display: block;\n font-size: 14px;\n line-height: 20px;\n min-height: 20px;\n color: #2d3748;\n font-weight: 500;\n word-break: break-all;\n}\n\n.recur-form-group {\n margin-bottom: 16px;\n}\n\n.recur-form-label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n.recur-form-input {\n width: 100%;\n height: 36px;\n padding: 0 12px;\n font-size: 14px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n transition: border-color 0.2s;\n box-sizing: border-box;\n}\n\n.recur-form-input:focus {\n border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.recur-form-input:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n/* ============================================\n * Card Fields Styles\n * ============================================ */\n.recur-card-field {\n margin-bottom: 16px;\n}\n\n.recur-card-field-label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n.recur-payuni-iframe {\n height: 36px;\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n touch-action: manipulation;\n}\n\n.recur-card-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n column-gap: 16px;\n max-width: 100%;\n box-sizing: border-box;\n}\n\n.recur-card-field-container {\n position: relative;\n touch-action: manipulation;\n}\n\n.recur-card-skeleton {\n position: absolute;\n top: 0;\n left: 0;\n height: 36px;\n width: 100%;\n background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 50%, #f0f0f0 100%);\n background-size: 200% 100%;\n animation: recur-skeleton-loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n z-index: 1;\n pointer-events: none;\n}\n\n.recur-card-skeleton.hidden {\n display: none;\n}\n\n@keyframes recur-skeleton-loading {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n\n/* Card verification notice ($0 total, card still bound for future charges) */\n.recur-verification-notice {\n border: 1px solid #bfdbfe;\n background-color: #eff6ff;\n color: #1e40af;\n border-radius: 8px;\n padding: 12px 16px;\n font-size: 14px;\n font-weight: 500;\n margin-bottom: 16px;\n}\n\n.recur-verification-notice.hidden {\n display: none;\n}\n\n/* ============================================\n * Submit Button Styles\n * ============================================ */\n.recur-submit-button {\n width: 100%;\n padding: 14px 20px;\n font-size: 16px;\n font-weight: 500;\n color: #ffffff;\n background: #18181b;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n box-sizing: border-box;\n}\n\n.recur-submit-button:hover:not(:disabled) {\n background: #27272a;\n}\n\n.recur-submit-button:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.recur-submit-button.loading {\n cursor: wait;\n}\n\n.recur-loading-spinner {\n width: 16px;\n height: 16px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n animation: recur-spin 0.6s linear infinite;\n}\n\n@keyframes recur-spin {\n to { transform: rotate(360deg); }\n}\n\n/* ============================================\n * Skeleton Loading Styles\n * ============================================ */\n.recur-skeleton {\n display: inline-block;\n background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 50%, #f0f0f0 100%);\n background-size: 200% 100%;\n animation: recur-skeleton-loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n}\n\n/* ============================================\n * Test Mode Banner Styles\n * ============================================ */\n.recur-test-mode-banner {\n background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);\n border: 1px solid #f59e0b;\n border-radius: 8px;\n margin-bottom: 16px;\n overflow: hidden;\n}\n\n.recur-test-mode-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 10px 14px;\n cursor: pointer;\n user-select: none;\n transition: background 0.2s;\n}\n\n.recur-test-mode-header:hover {\n background: rgba(245, 158, 11, 0.1);\n}\n\n.recur-test-mode-badge {\n font-size: 13px;\n font-weight: 600;\n color: #92400e;\n}\n\n.recur-test-mode-toggle {\n font-size: 12px;\n color: #b45309;\n}\n\n.recur-test-cards {\n background: rgba(255, 255, 255, 0.7);\n padding: 12px 14px;\n border-top: 1px solid rgba(245, 158, 11, 0.3);\n}\n\n.recur-test-cards-info {\n font-size: 12px;\n color: #78350f;\n margin: 0 0 10px 0;\n}\n\n.recur-test-card {\n background: white;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n padding: 10px 12px;\n margin-bottom: 8px;\n}\n\n.recur-test-card:last-child {\n margin-bottom: 0;\n}\n\n.recur-test-card-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 4px;\n}\n\n.recur-test-card-number {\n font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;\n font-size: 14px;\n font-weight: 600;\n color: #1f2937;\n letter-spacing: 0.5px;\n}\n\n.recur-test-card-copy {\n font-size: 11px;\n font-weight: 500;\n color: #3b82f6;\n background: #eff6ff;\n border: 1px solid #bfdbfe;\n border-radius: 4px;\n padding: 3px 8px;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n.recur-test-card-copy:hover {\n background: #dbeafe;\n border-color: #93c5fd;\n}\n\n.recur-test-card-copy.copied {\n background: #dcfce7;\n border-color: #86efac;\n color: #16a34a;\n}\n\n.recur-test-card-details {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n font-size: 11px;\n color: #6b7280;\n}\n\n.recur-test-card-desc {\n color: #374151;\n font-weight: 500;\n}\n\n.recur-test-card-meta {\n color: #9ca3af;\n}\n";
1082
1486
  }));
1083
1487
 
1084
1488
  //#endregion
@@ -1107,6 +1511,7 @@ function formatPrice(amount) {
1107
1511
  function getCheckoutDiscountDisplay(input) {
1108
1512
  const { discountType, couponDiscountValue, discountAmount, duration, durationMonths, productPrice, productInterval, productIntervalCount, bonusMonths, bonusTrialDays } = input;
1109
1513
  const intervalText = getIntervalText(productInterval, productIntervalCount);
1514
+ const isOneTime = !intervalText;
1110
1515
  const originalPriceText = intervalText ? `${formatPrice(productPrice)}/${intervalText}` : formatPrice(productPrice);
1111
1516
  const bonusSuffix = buildBonusSuffix(bonusMonths, bonusTrialDays);
1112
1517
  if (discountAmount === 0 && (bonusMonths || bonusTrialDays)) return {
@@ -1121,9 +1526,10 @@ function getCheckoutDiscountDisplay(input) {
1121
1526
  if (discountType === "FIRST_PERIOD_PRICE") {
1122
1527
  const firstPeriodPrice = couponDiscountValue;
1123
1528
  const priceText = firstPeriodPrice === 0 ? "免費" : `特價 ${formatPrice(firstPeriodPrice)}`;
1529
+ const base = isOneTime ? priceText : `首期${priceText}`;
1124
1530
  return {
1125
- line1: bonusSuffix ? `首期${priceText},${bonusSuffix}` : `首期${priceText}`,
1126
- line2: afterLine
1531
+ line1: bonusSuffix ? `${base},${bonusSuffix}` : base,
1532
+ line2: isOneTime ? null : afterLine
1127
1533
  };
1128
1534
  }
1129
1535
  if (discountType === "FIXED_AMOUNT") return buildStandardDiscount({
@@ -1131,14 +1537,16 @@ function getCheckoutDiscountDisplay(input) {
1131
1537
  duration,
1132
1538
  durationMonths,
1133
1539
  bonusSuffix,
1134
- afterLine
1540
+ afterLine,
1541
+ isOneTime
1135
1542
  });
1136
1543
  if (discountType === "PERCENTAGE") return buildStandardDiscount({
1137
- discountText: `享 ${formatZhe(couponDiscountValue)}`,
1544
+ discountText: couponDiscountValue >= 1e4 ? "免費" : `享 ${formatZhe(couponDiscountValue)}`,
1138
1545
  duration,
1139
1546
  durationMonths,
1140
1547
  bonusSuffix,
1141
- afterLine
1548
+ afterLine,
1549
+ isOneTime
1142
1550
  });
1143
1551
  return {
1144
1552
  line1: "",
@@ -1146,7 +1554,11 @@ function getCheckoutDiscountDisplay(input) {
1146
1554
  };
1147
1555
  }
1148
1556
  function buildStandardDiscount(opts) {
1149
- const { discountText, duration, durationMonths, bonusSuffix, afterLine } = opts;
1557
+ const { discountText, duration, durationMonths, bonusSuffix, afterLine, isOneTime } = opts;
1558
+ if (isOneTime) return {
1559
+ line1: bonusSuffix ? `${discountText},${bonusSuffix}` : discountText,
1560
+ line2: null
1561
+ };
1150
1562
  switch (duration) {
1151
1563
  case "ONCE": {
1152
1564
  const base = `首期${discountText}`;
@@ -1268,7 +1680,9 @@ var init_payment_form = __esmMin((() => {
1268
1680
  "interval",
1269
1681
  "checkout-id",
1270
1682
  "publishable-key",
1271
- "api-base-url"
1683
+ "api-base-url",
1684
+ "einvoice-enabled",
1685
+ "einvoice-defaults"
1272
1686
  ];
1273
1687
  }
1274
1688
  attributeChangedCallback(name, oldValue, newValue) {
@@ -1276,8 +1690,13 @@ var init_payment_form = __esmMin((() => {
1276
1690
  if (name === "custom-styles") {
1277
1691
  this.customStyles = newValue || "";
1278
1692
  this.updateCustomStyles();
1693
+ } else if (name === "einvoice-enabled" || name === "einvoice-defaults") {
1694
+ if (this.isConnected) this.render();
1279
1695
  } else this.updateOrderSummaryDOM();
1280
1696
  }
1697
+ isEinvoiceEnabled() {
1698
+ return this.getAttribute("einvoice-enabled") === "true";
1699
+ }
1281
1700
  getShadowDOMStyles() {
1282
1701
  return payment_form_shadow_default;
1283
1702
  }
@@ -1309,6 +1728,13 @@ var init_payment_form = __esmMin((() => {
1309
1728
  <slot name="customer-info"></slot>
1310
1729
  </div>
1311
1730
 
1731
+ ${this.isEinvoiceEnabled() ? lit_html.html`
1732
+ <div class="form-section">
1733
+ <h3 class="section-title">發票資訊</h3>
1734
+ <slot name="einvoice"></slot>
1735
+ </div>
1736
+ ` : lit_html.nothing}
1737
+
1312
1738
  <div class="form-section">
1313
1739
  <h3 class="section-title">信用卡資訊</h3>
1314
1740
  <slot name="card-fields"></slot>
@@ -1342,6 +1768,15 @@ var init_payment_form = __esmMin((() => {
1342
1768
  ${this.renderCustomerInfo()}
1343
1769
  </div>
1344
1770
 
1771
+ ${this.isEinvoiceEnabled() ? lit_html.html`
1772
+ <div slot="einvoice">
1773
+ <recur-einvoice-section
1774
+ id="${this.containerId}-einvoice"
1775
+ defaults=${this.getAttribute("einvoice-defaults") ?? lit_html.nothing}
1776
+ ></recur-einvoice-section>
1777
+ </div>
1778
+ ` : lit_html.nothing}
1779
+
1345
1780
  <div slot="card-fields">
1346
1781
  ${this.renderCardFields()}
1347
1782
  </div>
@@ -1737,6 +2172,7 @@ var init_payment_form = __esmMin((() => {
1737
2172
  const totalValue = summaryContainer.querySelector(".recur-order-summary__row--total")?.querySelector(".recur-order-summary__total");
1738
2173
  if (totalValue) totalValue.textContent = `NT$ ${finalAmount.toLocaleString()}`;
1739
2174
  this.updateCouponSectionDOM();
2175
+ this.updateVerificationUI();
1740
2176
  }
1741
2177
  async applyCoupon() {
1742
2178
  const code = this._couponCode.trim();
@@ -1875,8 +2311,41 @@ var init_payment_form = __esmMin((() => {
1875
2311
  </div>
1876
2312
  `;
1877
2313
  }
2314
+ /** Final payable amount after any applied coupon (never negative). */
2315
+ getFinalAmount() {
2316
+ const amount = Number(this.getAttribute("amount") || 0);
2317
+ const discountAmount = this._appliedCoupon?.discountAmount || 0;
2318
+ return Math.max(0, amount - discountAmount);
2319
+ }
2320
+ /**
2321
+ * $0 total but the card form is still shown: the backend runs a NT$2
2322
+ * verification charge (voided afterwards) to bind the card for future
2323
+ * charges. The UI must say 驗證 instead of 付款.
2324
+ */
2325
+ isCardVerificationMode() {
2326
+ const amountAttr = this.getAttribute("amount");
2327
+ if (amountAttr === null || amountAttr === "") return false;
2328
+ return this.getFinalAmount() === 0;
2329
+ }
2330
+ getSubmitLabel() {
2331
+ return this.isCardVerificationMode() ? "驗證卡片" : "確認付款";
2332
+ }
2333
+ /** Sync the verification notice + submit label with the current amount. */
2334
+ updateVerificationUI() {
2335
+ const notice = document.getElementById(`${this.containerId}-verification-notice`);
2336
+ if (notice) notice.classList.toggle("hidden", !this.isCardVerificationMode());
2337
+ const submitText = document.getElementById(`${this.containerId}-submit-text`);
2338
+ if (submitText) submitText.textContent = this.getSubmitLabel();
2339
+ }
1878
2340
  renderCardFields() {
1879
2341
  return lit_html.html`
2342
+ <div
2343
+ id="${this.containerId}-verification-notice"
2344
+ class="recur-verification-notice${this.isCardVerificationMode() ? "" : " hidden"}"
2345
+ >
2346
+ 您的優惠碼已抵扣全額,仍需驗證信用卡以供日後扣款使用
2347
+ </div>
2348
+
1880
2349
  <div class="recur-card-field">
1881
2350
  <label class="recur-card-field-label">卡號</label>
1882
2351
  <div class="recur-card-field-container" id="${this.containerId}-card-no-wrapper">
@@ -1922,7 +2391,7 @@ var init_payment_form = __esmMin((() => {
1922
2391
  class="recur-submit-button"
1923
2392
  disabled
1924
2393
  >
1925
- <span id="${this.containerId}-submit-text">確認付款</span>
2394
+ <span id="${this.containerId}-submit-text">${this.getSubmitLabel()}</span>
1926
2395
  </button>
1927
2396
  `;
1928
2397
  }
@@ -2040,6 +2509,18 @@ var init_payment_form = __esmMin((() => {
2040
2509
  return;
2041
2510
  }
2042
2511
  }
2512
+ let einvoice;
2513
+ if (this.isEinvoiceEnabled()) {
2514
+ const einvoiceSection = this.querySelector("recur-einvoice-section");
2515
+ if (einvoiceSection) {
2516
+ const prefs = einvoiceSection.getValidatedPrefs();
2517
+ if (!prefs) {
2518
+ this.showError("請確認發票資訊");
2519
+ return;
2520
+ }
2521
+ einvoice = prefs;
2522
+ }
2523
+ }
2043
2524
  this.clearError();
2044
2525
  this.setButtonLoading(true);
2045
2526
  this.dispatchEvent(new CustomEvent("submit", {
@@ -2047,7 +2528,8 @@ var init_payment_form = __esmMin((() => {
2047
2528
  customerEmail: email,
2048
2529
  customerName: name,
2049
2530
  paymentSession: this._paymentSession,
2050
- appliedCoupon: this._appliedCoupon
2531
+ appliedCoupon: this._appliedCoupon,
2532
+ einvoice
2051
2533
  },
2052
2534
  bubbles: true,
2053
2535
  composed: true
@@ -2067,7 +2549,7 @@ var init_payment_form = __esmMin((() => {
2067
2549
  } else {
2068
2550
  submitBtn.classList.remove("loading");
2069
2551
  submitBtn.disabled = false;
2070
- submitBtn.innerHTML = "<span id=\"" + this.containerId + "-submit-text\">確認付款</span>";
2552
+ submitBtn.innerHTML = "<span id=\"" + this.containerId + "-submit-text\">" + this.getSubmitLabel() + "</span>";
2071
2553
  }
2072
2554
  }
2073
2555
  resetButton() {
@@ -2669,6 +3151,7 @@ async function registerComponents() {
2669
3151
  Promise.resolve().then(() => (init_skeleton_loader(), skeleton_loader_exports)),
2670
3152
  Promise.resolve().then(() => (init_payment_form_skeleton(), payment_form_skeleton_exports)),
2671
3153
  Promise.resolve().then(() => (init_toast(), toast_exports)),
3154
+ Promise.resolve().then(() => (init_einvoice_section(), einvoice_section_exports)),
2672
3155
  Promise.resolve().then(() => (init_payment_form(), payment_form_exports)),
2673
3156
  Promise.resolve().then(() => (init_checkout_button(), checkout_button_exports)),
2674
3157
  Promise.resolve().then(() => (init_portal_button(), portal_button_exports))
@@ -2681,6 +3164,7 @@ async function registerComponents() {
2681
3164
  "recur-payment-form-skeleton",
2682
3165
  "recur-toast",
2683
3166
  "recur-toast-container",
3167
+ "recur-einvoice-section",
2684
3168
  "recur-payment-form",
2685
3169
  "recur-checkout",
2686
3170
  "recur-portal"
@@ -2765,10 +3249,11 @@ function toCamelCase(obj) {
2765
3249
 
2766
3250
  //#endregion
2767
3251
  //#region package.json
2768
- var version = "0.17.0";
3252
+ var version = "0.19.0";
2769
3253
 
2770
3254
  //#endregion
2771
3255
  //#region src/context.tsx
3256
+ init_einvoice();
2772
3257
  const SDK_VERSION$1 = version;
2773
3258
  const SDK_TYPE$1 = "react";
2774
3259
  const RecurContext = (0, react.createContext)(null);
@@ -3108,6 +3593,10 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3108
3593
  if (options.externalCustomerId) checkoutRequestBody.external_customer_id = options.externalCustomerId;
3109
3594
  if (options.successUrl) checkoutRequestBody.success_url = options.successUrl;
3110
3595
  if (options.cancelUrl) checkoutRequestBody.cancel_url = options.cancelUrl;
3596
+ if (options.einvoice) {
3597
+ assertValidEinvoicePrefs(options.einvoice);
3598
+ checkoutRequestBody.einvoice = options.einvoice;
3599
+ }
3111
3600
  const checkoutResponse = await fetch(`${baseUrl}/v1/checkouts`, {
3112
3601
  method: "POST",
3113
3602
  headers,
@@ -3191,6 +3680,11 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3191
3680
  if (checkoutResult.product?.name) paymentForm.setAttribute("product-name", checkoutResult.product.name);
3192
3681
  if (checkoutResult.checkout?.amount) paymentForm.setAttribute("amount", checkoutResult.checkout.amount.toString());
3193
3682
  if (checkoutResult.product?.interval) paymentForm.setAttribute("interval", checkoutResult.product.interval);
3683
+ if (checkoutResult.einvoiceEnabled) {
3684
+ paymentForm.setAttribute("einvoice-enabled", "true");
3685
+ const einvoiceDefaults = options.einvoice ?? checkoutResult.einvoiceDefaults;
3686
+ if (einvoiceDefaults) paymentForm.setAttribute("einvoice-defaults", JSON.stringify(einvoiceDefaults));
3687
+ }
3194
3688
  paymentForm.setAttribute("custom-styles", `
3195
3689
  .form-input-focus {
3196
3690
  border-color: var(--ring, hsl(215 16% 47%)) !important;
@@ -3210,7 +3704,7 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3210
3704
  console.log("[Recur SDK] Step 7: Setting up submit handler...");
3211
3705
  paymentForm.addEventListener("submit", (async (event) => {
3212
3706
  console.log("[Recur SDK] Form submitted via Web Component");
3213
- const { paymentSession } = event.detail;
3707
+ const { paymentSession, einvoice } = event.detail;
3214
3708
  try {
3215
3709
  console.log("[Recur SDK] Getting trade result from PAYUNi...");
3216
3710
  const tradeResult = await paymentSession.getTradeResult();
@@ -3252,6 +3746,7 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3252
3746
  console.log("[Recur SDK] Using creditToken from checkout:", subscriptionCreditToken.substring(0, 30) + "...");
3253
3747
  console.log("[Recur SDK] Using timestamp:", subscriptionTimestamp ? "from checkout (sdkTimestamp)" : "from tradeResult");
3254
3748
  }
3749
+ if (einvoice) paymentBody.einvoice = einvoice;
3255
3750
  const paymentResponse = await fetch(`${baseUrl}/v1/checkouts/${checkoutResult.checkout.id}/pay`, {
3256
3751
  method: "POST",
3257
3752
  headers,
@@ -3559,6 +4054,10 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3559
4054
  if (options.customerEmail) requestBody.customerEmail = options.customerEmail;
3560
4055
  if (options.customerName) requestBody.customerName = options.customerName;
3561
4056
  if (options.externalCustomerId) requestBody.externalCustomerId = options.externalCustomerId;
4057
+ if (options.einvoice) {
4058
+ assertValidEinvoicePrefs(options.einvoice);
4059
+ requestBody.einvoice = options.einvoice;
4060
+ }
3562
4061
  const response = await fetch(`${baseUrl}/v1/checkout/sessions`, {
3563
4062
  method: "POST",
3564
4063
  headers: {
@@ -3579,7 +4078,8 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3579
4078
  id: result.id,
3580
4079
  url: result.url,
3581
4080
  expiresAt: result.expiresAt,
3582
- clientSecret: result.clientSecret
4081
+ clientSecret: result.clientSecret,
4082
+ einvoiceEnabled: result.einvoiceEnabled
3583
4083
  };
3584
4084
  }, [config]);
3585
4085
  const redirectToCheckout = (0, react.useCallback)(async (options) => {
@@ -4331,11 +4831,21 @@ function useCustomer() {
4331
4831
  }
4332
4832
 
4333
4833
  //#endregion
4834
+ //#region src/index.ts
4835
+ init_einvoice();
4836
+
4837
+ //#endregion
4838
+ exports.DONATION_CODE_RE = DONATION_CODE_RE;
4839
+ exports.MOBILE_BARCODE_RE = MOBILE_BARCODE_RE;
4334
4840
  exports.PromoCodeInput = PromoCodeInput;
4335
4841
  exports.RecurProvider = RecurProvider;
4842
+ exports.UBN_RE = UBN_RE;
4843
+ exports.assertValidEinvoicePrefs = assertValidEinvoicePrefs;
4844
+ exports.isValidTaiwanUbn = isValidTaiwanUbn;
4336
4845
  exports.useCustomer = useCustomer;
4337
4846
  exports.usePlans = useProducts;
4338
4847
  exports.useProducts = useProducts;
4339
4848
  exports.usePromoCode = usePromoCode;
4340
4849
  exports.useRecur = useRecur;
4341
- exports.useSubscribe = useSubscribe;
4850
+ exports.useSubscribe = useSubscribe;
4851
+ exports.validateEinvoicePrefs = validateEinvoicePrefs;