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 +457 -61
- package/dist/index.d.cts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +457 -61
- package/dist/recur.umd.js +179 -63
- package/dist/server.cjs +42 -1
- package/dist/server.d.cts +9 -2
- package/dist/server.d.ts +9 -2
- package/dist/server.js +42 -2
- package/package.json +25 -8
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.
|
|
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;
|