recur-tw 0.18.0 → 0.20.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
@@ -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;
@@ -1276,7 +1680,9 @@ var init_payment_form = __esmMin((() => {
1276
1680
  "interval",
1277
1681
  "checkout-id",
1278
1682
  "publishable-key",
1279
- "api-base-url"
1683
+ "api-base-url",
1684
+ "einvoice-enabled",
1685
+ "einvoice-defaults"
1280
1686
  ];
1281
1687
  }
1282
1688
  attributeChangedCallback(name, oldValue, newValue) {
@@ -1284,8 +1690,13 @@ var init_payment_form = __esmMin((() => {
1284
1690
  if (name === "custom-styles") {
1285
1691
  this.customStyles = newValue || "";
1286
1692
  this.updateCustomStyles();
1693
+ } else if (name === "einvoice-enabled" || name === "einvoice-defaults") {
1694
+ if (this.isConnected) this.render();
1287
1695
  } else this.updateOrderSummaryDOM();
1288
1696
  }
1697
+ isEinvoiceEnabled() {
1698
+ return this.getAttribute("einvoice-enabled") === "true";
1699
+ }
1289
1700
  getShadowDOMStyles() {
1290
1701
  return payment_form_shadow_default;
1291
1702
  }
@@ -1317,6 +1728,13 @@ var init_payment_form = __esmMin((() => {
1317
1728
  <slot name="customer-info"></slot>
1318
1729
  </div>
1319
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
+
1320
1738
  <div class="form-section">
1321
1739
  <h3 class="section-title">信用卡資訊</h3>
1322
1740
  <slot name="card-fields"></slot>
@@ -1350,6 +1768,15 @@ var init_payment_form = __esmMin((() => {
1350
1768
  ${this.renderCustomerInfo()}
1351
1769
  </div>
1352
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
+
1353
1780
  <div slot="card-fields">
1354
1781
  ${this.renderCardFields()}
1355
1782
  </div>
@@ -2082,6 +2509,18 @@ var init_payment_form = __esmMin((() => {
2082
2509
  return;
2083
2510
  }
2084
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
+ }
2085
2524
  this.clearError();
2086
2525
  this.setButtonLoading(true);
2087
2526
  this.dispatchEvent(new CustomEvent("submit", {
@@ -2089,7 +2528,8 @@ var init_payment_form = __esmMin((() => {
2089
2528
  customerEmail: email,
2090
2529
  customerName: name,
2091
2530
  paymentSession: this._paymentSession,
2092
- appliedCoupon: this._appliedCoupon
2531
+ appliedCoupon: this._appliedCoupon,
2532
+ einvoice
2093
2533
  },
2094
2534
  bubbles: true,
2095
2535
  composed: true
@@ -2711,6 +3151,7 @@ async function registerComponents() {
2711
3151
  Promise.resolve().then(() => (init_skeleton_loader(), skeleton_loader_exports)),
2712
3152
  Promise.resolve().then(() => (init_payment_form_skeleton(), payment_form_skeleton_exports)),
2713
3153
  Promise.resolve().then(() => (init_toast(), toast_exports)),
3154
+ Promise.resolve().then(() => (init_einvoice_section(), einvoice_section_exports)),
2714
3155
  Promise.resolve().then(() => (init_payment_form(), payment_form_exports)),
2715
3156
  Promise.resolve().then(() => (init_checkout_button(), checkout_button_exports)),
2716
3157
  Promise.resolve().then(() => (init_portal_button(), portal_button_exports))
@@ -2723,6 +3164,7 @@ async function registerComponents() {
2723
3164
  "recur-payment-form-skeleton",
2724
3165
  "recur-toast",
2725
3166
  "recur-toast-container",
3167
+ "recur-einvoice-section",
2726
3168
  "recur-payment-form",
2727
3169
  "recur-checkout",
2728
3170
  "recur-portal"
@@ -2805,69 +3247,13 @@ function toCamelCase(obj) {
2805
3247
  return obj;
2806
3248
  }
2807
3249
 
2808
- //#endregion
2809
- //#region src/einvoice.ts
2810
- /** 手機條碼: `/` + 7 chars from the MOF alphabet. */
2811
- const MOBILE_BARCODE_RE = /^\/[0-9A-Z.+-]{7}$/;
2812
- /** 統一編號: 8 digits. */
2813
- const UBN_RE = /^\d{8}$/;
2814
- /** 愛心碼: 3–7 digits. */
2815
- const DONATION_CODE_RE = /^\d{3,7}$/;
2816
- /**
2817
- * 統一編號 checksum (post-2023 rule: weighted digit-sum divisible by 5, with
2818
- * the 7-in-7th-digit alternate). Providers reject checksum-invalid UBNs at
2819
- * issue time as a non-retryable failure, so the SDK catches this at entry.
2820
- */
2821
- function isValidTaiwanUbn(ubn) {
2822
- if (!UBN_RE.test(ubn)) return false;
2823
- const weights = [
2824
- 1,
2825
- 2,
2826
- 1,
2827
- 2,
2828
- 1,
2829
- 2,
2830
- 4,
2831
- 1
2832
- ];
2833
- let sum = 0;
2834
- for (let i = 0; i < 8; i++) {
2835
- const product = Number(ubn.charAt(i)) * (weights[i] ?? 0);
2836
- sum += Math.floor(product / 10) + product % 10;
2837
- }
2838
- if (sum % 5 === 0) return true;
2839
- return ubn.charAt(6) === "7" && (sum + 1) % 5 === 0;
2840
- }
2841
- /**
2842
- * Validate buyer invoice preferences.
2843
- *
2844
- * @returns an error message, or `null` when the prefs are valid
2845
- */
2846
- function validateEinvoicePrefs(prefs) {
2847
- if (typeof prefs !== "object" || prefs === null) return "Invalid einvoice prefs: expected an object like { type: \"personal\" }";
2848
- switch (prefs.type) {
2849
- case "personal": return null;
2850
- 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, ., +, -)";
2851
- case "ubn":
2852
- if (!isValidTaiwanUbn(prefs.ubn)) return "Invalid einvoice UBN (統一編號): must be 8 digits with a valid checksum";
2853
- if (!prefs.buyerName || !prefs.buyerName.trim() || prefs.buyerName.trim().length > 60) return "Invalid einvoice buyerName (公司抬頭): required, at most 60 characters";
2854
- return null;
2855
- case "donation": return DONATION_CODE_RE.test(prefs.donationCode) ? null : "Invalid einvoice donation code (愛心碼): must be 3-7 digits";
2856
- default: return `Invalid einvoice type: ${String(prefs.type)}`;
2857
- }
2858
- }
2859
- /** Throw when buyer invoice preferences are invalid (fail fast, before the API call). */
2860
- function assertValidEinvoicePrefs(prefs) {
2861
- const error = validateEinvoicePrefs(prefs);
2862
- if (error) throw new Error(error);
2863
- }
2864
-
2865
3250
  //#endregion
2866
3251
  //#region package.json
2867
- var version = "0.18.0";
3252
+ var version = "0.20.0";
2868
3253
 
2869
3254
  //#endregion
2870
3255
  //#region src/context.tsx
3256
+ init_einvoice();
2871
3257
  const SDK_VERSION$1 = version;
2872
3258
  const SDK_TYPE$1 = "react";
2873
3259
  const RecurContext = (0, react.createContext)(null);
@@ -3294,6 +3680,11 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3294
3680
  if (checkoutResult.product?.name) paymentForm.setAttribute("product-name", checkoutResult.product.name);
3295
3681
  if (checkoutResult.checkout?.amount) paymentForm.setAttribute("amount", checkoutResult.checkout.amount.toString());
3296
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
+ }
3297
3688
  paymentForm.setAttribute("custom-styles", `
3298
3689
  .form-input-focus {
3299
3690
  border-color: var(--ring, hsl(215 16% 47%)) !important;
@@ -3313,7 +3704,7 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3313
3704
  console.log("[Recur SDK] Step 7: Setting up submit handler...");
3314
3705
  paymentForm.addEventListener("submit", (async (event) => {
3315
3706
  console.log("[Recur SDK] Form submitted via Web Component");
3316
- const { paymentSession } = event.detail;
3707
+ const { paymentSession, einvoice } = event.detail;
3317
3708
  try {
3318
3709
  console.log("[Recur SDK] Getting trade result from PAYUNi...");
3319
3710
  const tradeResult = await paymentSession.getTradeResult();
@@ -3355,6 +3746,7 @@ function RecurProvider({ children, config: initialConfig = {}, customer: custome
3355
3746
  console.log("[Recur SDK] Using creditToken from checkout:", subscriptionCreditToken.substring(0, 30) + "...");
3356
3747
  console.log("[Recur SDK] Using timestamp:", subscriptionTimestamp ? "from checkout (sdkTimestamp)" : "from tradeResult");
3357
3748
  }
3749
+ if (einvoice) paymentBody.einvoice = einvoice;
3358
3750
  const paymentResponse = await fetch(`${baseUrl}/v1/checkouts/${checkoutResult.checkout.id}/pay`, {
3359
3751
  method: "POST",
3360
3752
  headers,
@@ -4438,6 +4830,10 @@ function useCustomer() {
4438
4830
  };
4439
4831
  }
4440
4832
 
4833
+ //#endregion
4834
+ //#region src/index.ts
4835
+ init_einvoice();
4836
+
4441
4837
  //#endregion
4442
4838
  exports.DONATION_CODE_RE = DONATION_CODE_RE;
4443
4839
  exports.MOBILE_BARCODE_RE = MOBILE_BARCODE_RE;
package/dist/index.d.cts CHANGED
@@ -210,6 +210,40 @@ declare function validateEinvoicePrefs(prefs: EinvoicePrefs): string | null;
210
210
  /** Throw when buyer invoice preferences are invalid (fail fast, before the API call). */
211
211
  declare function assertValidEinvoicePrefs(prefs: EinvoicePrefs): void;
212
212
  //#endregion
213
+ //#region src/components/einvoice-section.d.ts
214
+ declare class RecurEinvoiceSection extends HTMLElement {
215
+ private selectedType;
216
+ private values;
217
+ private touched;
218
+ constructor();
219
+ static get observedAttributes(): string[];
220
+ connectedCallback(): void;
221
+ attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
222
+ private applyDefaults;
223
+ private buildPrefs;
224
+ private fieldError;
225
+ private isValid;
226
+ /**
227
+ * Validate the current selection, surfacing inline errors.
228
+ * @returns the prefs when valid, null otherwise
229
+ */
230
+ getValidatedPrefs(): EinvoicePrefs | null;
231
+ private emitChange;
232
+ private selectType;
233
+ /** ARIA radio keyboard pattern: arrow keys move selection (with wrap). */
234
+ private onOptionKeydown;
235
+ private onInput;
236
+ private onBlur;
237
+ private renderField;
238
+ private renderFields;
239
+ private render;
240
+ }
241
+ declare global {
242
+ interface HTMLElementTagNameMap {
243
+ 'recur-einvoice-section': RecurEinvoiceSection;
244
+ }
245
+ }
246
+ //#endregion
213
247
  //#region src/types.d.ts
214
248
  interface RecurConfig {
215
249
  /**
@@ -427,6 +461,12 @@ interface CheckoutResult {
427
461
  livemode?: boolean;
428
462
  /** Whether the organization issues e-invoices for paid checkouts */
429
463
  einvoiceEnabled?: boolean;
464
+ /**
465
+ * Server-computed e-invoice prefill: the stored choice on this checkout,
466
+ * else the customer's remembered defaults, else personal. Null when
467
+ * e-invoice is not enabled.
468
+ */
469
+ einvoiceDefaults?: EinvoicePrefs | null;
430
470
  }
431
471
  /**
432
472
  * Payment failure codes from payment provider
@@ -1090,6 +1130,7 @@ declare class RecurPaymentForm extends HTMLElement {
1090
1130
  disconnectedCallback(): void;
1091
1131
  static get observedAttributes(): string[];
1092
1132
  attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
1133
+ private isEinvoiceEnabled;
1093
1134
  private getShadowDOMStyles;
1094
1135
  private getLightDOMStyles;
1095
1136
  private render;