@kwespay/widget 1.0.6 → 1.0.7

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/esm/index.js CHANGED
@@ -97,6 +97,7 @@ const TOKEN_CONFIGS = {
97
97
  decimals: 18,
98
98
  coingeckoId: "ethereum",
99
99
  binanceSymbol: "ETHUSDT",
100
+
100
101
  },
101
102
  {
102
103
  symbol: "USDT",
@@ -1006,6 +1007,84 @@ class PaymentService {
1006
1007
  }
1007
1008
  }
1008
1009
 
1010
+ function isMobileDevice() {
1011
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
1012
+ navigator.userAgent
1013
+ );
1014
+ }
1015
+
1016
+ function dispatchWidgetEvent(eventName, detail = {}) {
1017
+ const event = new CustomEvent(`kwespay:${eventName}`, {
1018
+ detail,
1019
+ bubbles: true,
1020
+ cancelable: true,
1021
+ });
1022
+ window.dispatchEvent(event);
1023
+ }
1024
+
1025
+ function truncateHash(hash, startChars = 10, endChars = 8) {
1026
+ if (!hash) return "";
1027
+ return `${hash.slice(0, startChars)}...${hash.slice(-endChars)}`;
1028
+ }
1029
+
1030
+ function getErrorType(error) {
1031
+ const msg = error?.message ?? "";
1032
+ if (
1033
+ error?.code === 4001 ||
1034
+ error?.code === "ACTION_REJECTED" ||
1035
+ msg.includes("rejected") ||
1036
+ msg.includes("denied") ||
1037
+ msg.includes("cancelled")
1038
+ )
1039
+ return "USER_REJECTED";
1040
+ if (msg.toLowerCase().includes("insufficient")) return "INSUFFICIENT_BALANCE";
1041
+ if (
1042
+ msg.includes("User rejected") ||
1043
+ msg.includes("User closed modal") ||
1044
+ msg.includes("Connection request reset")
1045
+ )
1046
+ return "CONNECTION_REJECTED";
1047
+ if (msg.includes("timeout")) return "TIMEOUT";
1048
+ return "UNKNOWN";
1049
+ }
1050
+
1051
+ function getErrorMessage(error, context = {}) {
1052
+ const type = getErrorType(error);
1053
+
1054
+ console.error("[KwesPay] Payment error breakdown:", {
1055
+ errorType: type,
1056
+ message: error?.message,
1057
+ code: error?.code,
1058
+ data: error?.data,
1059
+ reason: error?.reason,
1060
+ stack: error?.stack,
1061
+ raw: error,
1062
+ context,
1063
+ });
1064
+
1065
+ if (error?.data) console.error("[KwesPay] Contract revert data:", error.data);
1066
+ if (error?.transaction)
1067
+ console.error("[KwesPay] Failed tx details:", error.transaction);
1068
+ if (error?.receipt) console.error("[KwesPay] Tx receipt:", error.receipt);
1069
+
1070
+ switch (type) {
1071
+ case "USER_REJECTED":
1072
+ return "You cancelled the transaction in your wallet.";
1073
+ case "INSUFFICIENT_BALANCE":
1074
+ return `Not enough ${context.token || "funds"} to complete this payment.`;
1075
+ case "CONNECTION_REJECTED":
1076
+ return "Wallet connection was cancelled.";
1077
+ case "TIMEOUT":
1078
+ return "Connection timed out. Please try again.";
1079
+ default:
1080
+ return (
1081
+ error?.reason ||
1082
+ error?.message ||
1083
+ "Something went wrong while processing your payment."
1084
+ );
1085
+ }
1086
+ }
1087
+
1009
1088
  const WIDGET_STYLES = `
1010
1089
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
1011
1090
 
@@ -2195,180 +2274,12 @@ function getStepTemplates(fiatAmount, currency) {
2195
2274
  `;
2196
2275
  }
2197
2276
 
2198
- function dispatchWidgetEvent(eventName, detail = {}) {
2199
- const event = new CustomEvent(`kwespay:${eventName}`, {
2200
- detail,
2201
- bubbles: true,
2202
- cancelable: true,
2203
- });
2204
- window.dispatchEvent(event);
2205
- }
2206
-
2207
- function truncateHash(hash, startChars = 10, endChars = 8) {
2208
- if (!hash) return "";
2209
- return `${hash.slice(0, startChars)}...${hash.slice(-endChars)}`;
2210
- }
2211
-
2212
- function getErrorType(error) {
2213
- const msg = error?.message ?? "";
2214
- if (
2215
- error?.code === 4001 ||
2216
- error?.code === "ACTION_REJECTED" ||
2217
- msg.includes("rejected") ||
2218
- msg.includes("denied") ||
2219
- msg.includes("cancelled")
2220
- )
2221
- return "USER_REJECTED";
2222
- if (msg.toLowerCase().includes("insufficient")) return "INSUFFICIENT_BALANCE";
2223
- if (
2224
- msg.includes("User rejected") ||
2225
- msg.includes("User closed modal") ||
2226
- msg.includes("Connection request reset")
2227
- )
2228
- return "CONNECTION_REJECTED";
2229
- if (msg.includes("timeout")) return "TIMEOUT";
2230
- return "UNKNOWN";
2231
- }
2232
-
2233
- function getErrorMessage(error, context = {}) {
2234
- const type = getErrorType(error);
2235
-
2236
- console.error("[KwesPay] Payment error breakdown:", {
2237
- errorType: type,
2238
- message: error?.message,
2239
- code: error?.code,
2240
- data: error?.data,
2241
- reason: error?.reason,
2242
- stack: error?.stack,
2243
- raw: error,
2244
- context,
2245
- });
2246
-
2247
- if (error?.data) console.error("[KwesPay] Contract revert data:", error.data);
2248
- if (error?.transaction)
2249
- console.error("[KwesPay] Failed tx details:", error.transaction);
2250
- if (error?.receipt) console.error("[KwesPay] Tx receipt:", error.receipt);
2251
-
2252
- switch (type) {
2253
- case "USER_REJECTED":
2254
- return "You cancelled the transaction in your wallet.";
2255
- case "INSUFFICIENT_BALANCE":
2256
- return `Not enough ${context.token || "funds"} to complete this payment.`;
2257
- case "CONNECTION_REJECTED":
2258
- return "Wallet connection was cancelled.";
2259
- case "TIMEOUT":
2260
- return "Connection timed out. Please try again.";
2261
- default:
2262
- return (
2263
- error?.reason ||
2264
- error?.message ||
2265
- "Something went wrong while processing your payment."
2266
- );
2267
- }
2268
- }
2269
-
2270
- const PLATFORM_FEE_BPS = 25;
2271
-
2272
- // ── Utilities ─────────────────────────────────────────────────────────────────
2273
-
2274
- function formatUnits(rawBigInt, decimals) {
2275
- const divisor = BigInt(10 ** decimals);
2276
- const whole = rawBigInt / divisor;
2277
- const remainder = rawBigInt % divisor;
2278
-
2279
- if (remainder === 0n) return `${whole}`;
2280
-
2281
- const fracFull = remainder.toString().padStart(decimals, "0");
2282
-
2283
- if (whole > 0n) {
2284
- const frac = fracFull.slice(0, 4).replace(/0+$/, "");
2285
- return frac ? `${whole}.${frac}` : `${whole}`;
2286
- }
2287
-
2288
- let firstSig = -1;
2289
- for (let i = 0; i < fracFull.length; i++) {
2290
- if (fracFull[i] !== "0") {
2291
- firstSig = i;
2292
- break;
2293
- }
2294
- }
2295
- if (firstSig === -1) return `${whole}`;
2296
-
2297
- const sigSlice = fracFull.slice(firstSig, firstSig + 4).replace(/0+$/, "");
2298
- return `0.${fracFull.slice(0, firstSig) + sigSlice}`;
2299
- }
2300
-
2301
- function resolveAcceptedTokens(input) {
2302
- if (!input) return null;
2303
- if (input === "stablecoins") return STABLECOIN_SYMBOLS;
2304
- if (Array.isArray(input) && input.length)
2305
- return input.map((t) => t.toUpperCase());
2306
- return null;
2307
- }
2308
-
2309
- function isMobileDevice() {
2310
- return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
2311
- }
2312
-
2313
- // ── Widget ────────────────────────────────────────────────────────────────────
2314
-
2315
- class KwesPayWidget {
2316
- constructor(config) {
2317
- if (!config.apiKey) throw new Error("[KwesPayWidget] apiKey is required");
2318
- if (!config.vendorId)
2319
- throw new Error("[KwesPayWidget] vendorId is required");
2320
- if (!config.amount || parseFloat(config.amount) <= 0)
2321
- throw new Error("[KwesPayWidget] Valid amount is required");
2322
-
2323
- this.config = {
2324
- apiKey: config.apiKey,
2325
- vendorId: config.vendorId,
2326
- amount: parseFloat(config.amount),
2327
- currency: config.currency || DEFAULT_CONFIG.currency,
2328
- graphqlEndpoint: DEFAULT_CONFIG.graphqlEndpoint,
2329
- acceptedTokens: resolveAcceptedTokens(config.acceptedTokens),
2330
- };
2331
-
2332
- if (!Object.values(SUPPORTED_CURRENCIES).includes(this.config.currency)) {
2333
- throw new Error(
2334
- `[KwesPayWidget] Unsupported currency: ${this.config.currency}`
2335
- );
2336
- }
2337
-
2338
- this.walletService = new WalletService();
2339
- this.paymentService = new PaymentService(
2340
- this.config.apiKey,
2341
- this.config.graphqlEndpoint
2342
- );
2343
-
2344
- this.state = {
2345
- isOpen: false,
2346
- currentStep: 0,
2347
- selectedNetwork: null,
2348
- selectedNetworkName: "",
2349
- selectedChainId: null,
2350
- selectedRpcUrl: null,
2351
- selectedContractAddress: null,
2352
- selectedToken: null,
2353
- selectedTokenConfig: null,
2354
- vendorInfo: null,
2355
- keyAllowedNetworks: null,
2356
- keyAllowedTokens: null,
2357
- currentPayload: null,
2358
- quoteTimerInterval: null,
2359
- wcUri: null,
2360
- };
2361
-
2362
- this._init();
2363
- }
2364
-
2365
- // ── Bootstrap ───────────────────────────────────────────────────────────────
2366
-
2277
+ const DomMethods = {
2367
2278
  _init() {
2368
2279
  this._injectFonts();
2369
2280
  this._injectStyles();
2370
2281
  this._createWidgetDOM();
2371
- }
2282
+ },
2372
2283
 
2373
2284
  _injectFonts() {
2374
2285
  if (
@@ -2382,7 +2293,7 @@ class KwesPayWidget {
2382
2293
  link.rel = "stylesheet";
2383
2294
  document.head.appendChild(link);
2384
2295
  }
2385
- }
2296
+ },
2386
2297
 
2387
2298
  _injectStyles() {
2388
2299
  if (!document.getElementById("kwespay-widget-styles")) {
@@ -2391,7 +2302,7 @@ class KwesPayWidget {
2391
2302
  style.textContent = WIDGET_STYLES;
2392
2303
  document.head.appendChild(style);
2393
2304
  }
2394
- }
2305
+ },
2395
2306
 
2396
2307
  _createWidgetDOM() {
2397
2308
  document.getElementById("kwespay-widget-overlay")?.remove();
@@ -2418,15 +2329,16 @@ class KwesPayWidget {
2418
2329
  document.body.appendChild(overlay);
2419
2330
 
2420
2331
  this._setupEventListeners();
2332
+ this._setupSwipeToClose(container);
2421
2333
 
2422
- overlay.addEventListener("click", (e) => {
2423
- if (e.target === overlay) this.close();
2424
- });
2334
+ // Clicking outside the container does NOT close the widget (intentional)
2335
+ },
2425
2336
 
2426
- // Swipe-down to close on mobile
2337
+ _setupSwipeToClose(container) {
2427
2338
  let _touchStartY = 0,
2428
2339
  _touchCurrentY = 0,
2429
2340
  _isDragging = false;
2341
+
2430
2342
  container.addEventListener(
2431
2343
  "touchstart",
2432
2344
  (e) => {
@@ -2441,6 +2353,7 @@ class KwesPayWidget {
2441
2353
  },
2442
2354
  { passive: true }
2443
2355
  );
2356
+
2444
2357
  container.addEventListener(
2445
2358
  "touchmove",
2446
2359
  (e) => {
@@ -2451,6 +2364,7 @@ class KwesPayWidget {
2451
2364
  },
2452
2365
  { passive: true }
2453
2366
  );
2367
+
2454
2368
  container.addEventListener("touchend", () => {
2455
2369
  if (!_isDragging) return;
2456
2370
  _isDragging = false;
@@ -2459,7 +2373,7 @@ class KwesPayWidget {
2459
2373
  if (delta > 120) this.close();
2460
2374
  else container.style.transform = "translateY(0)";
2461
2375
  });
2462
- }
2376
+ },
2463
2377
 
2464
2378
  _setupEventListeners() {
2465
2379
  document
@@ -2568,10 +2482,83 @@ class KwesPayWidget {
2568
2482
  });
2569
2483
  });
2570
2484
  }
2571
- }
2485
+ },
2486
+ };
2572
2487
 
2573
- // ── Network / token list rendering ──────────────────────────────────────────
2488
+ const NavMethods = {
2489
+ _goToStep(stepNumber) {
2490
+ document
2491
+ .querySelectorAll(".kwespay-container .step")
2492
+ .forEach((s) => s.classList.remove("active", "exiting"));
2493
+ this._activateStep(stepNumber);
2494
+ this.state.currentStep = stepNumber;
2495
+ },
2496
+
2497
+ _activateStep(stepNumber) {
2498
+ let targetStep;
2499
+ if (stepNumber === 0.5)
2500
+ targetStep = document.getElementById("kwespay-step0-invalid");
2501
+ else if (typeof stepNumber === "string")
2502
+ targetStep = document.getElementById(`kwespay-step-${stepNumber}`);
2503
+ else targetStep = document.getElementById(`kwespay-step${stepNumber}`);
2504
+
2505
+ if (targetStep) targetStep.classList.add("active");
2506
+ else console.warn(`[KwesPayWidget] Step not found: ${stepNumber}`);
2507
+ },
2508
+
2509
+ _removeCustomStep(id) {
2510
+ document.getElementById(id)?.remove();
2511
+ },
2574
2512
 
2513
+ _showError(title, message) {
2514
+ const titleEl = document.getElementById("kwespay-errorTitle");
2515
+ const msgEl = document.getElementById("kwespay-errorMessage");
2516
+ if (titleEl) titleEl.textContent = title;
2517
+ if (msgEl) msgEl.textContent = message;
2518
+ this._goToStep(6);
2519
+ },
2520
+
2521
+ _reset() {
2522
+ this._clearQuoteTimer();
2523
+ this._removeCustomStep("kwespay-step-wallet-picker");
2524
+ this._removeCustomStep("kwespay-step-wc");
2525
+ this.state.selectedNetwork = null;
2526
+ this.state.selectedNetworkName = "";
2527
+ this.state.selectedChainId = null;
2528
+ this.state.selectedRpcUrl = null;
2529
+ this.state.selectedContractAddress = null;
2530
+ this.state.selectedToken = null;
2531
+ this.state.selectedTokenConfig = null;
2532
+ this.state.currentPayload = null;
2533
+ this.state.wcUri = null;
2534
+ },
2535
+ };
2536
+
2537
+ const APIKeyMethods = {
2538
+ async _validateAPIKey() {
2539
+ try {
2540
+ const validation = await this.paymentService.validateAPIKey();
2541
+ if (validation.valid) {
2542
+ this.state.vendorInfo = validation.vendorInfo;
2543
+ this.state.keyAllowedNetworks = validation.allowedNetworks ?? null;
2544
+ this.state.keyAllowedTokens = validation.allowedTokens ?? null;
2545
+ this._goToStep(1);
2546
+ dispatchWidgetEvent("apiKeyValidated", {
2547
+ vendorInfo: this.state.vendorInfo,
2548
+ });
2549
+ } else {
2550
+ this._goToStep(0.5);
2551
+ dispatchWidgetEvent("apiKeyInvalid", {});
2552
+ }
2553
+ } catch (err) {
2554
+ console.error("[KwesPayWidget] API key validation error:", err.message);
2555
+ this._goToStep(0.5);
2556
+ dispatchWidgetEvent("apiKeyError", { error: err.message });
2557
+ }
2558
+ },
2559
+ };
2560
+
2561
+ const NetworkMethods = {
2575
2562
  _renderNetworkList() {
2576
2563
  const mainnetList = document.getElementById("kwespay-mainnetList");
2577
2564
  const testnetList = document.getElementById("kwespay-testnetList");
@@ -2633,7 +2620,7 @@ class KwesPayWidget {
2633
2620
 
2634
2621
  if (testnetSection)
2635
2622
  testnetSection.style.display = hasTestnet ? "block" : "none";
2636
- }
2623
+ },
2637
2624
 
2638
2625
  _renderTokenList() {
2639
2626
  if (!this.state.selectedNetwork) return;
@@ -2672,7 +2659,7 @@ class KwesPayWidget {
2672
2659
  );
2673
2660
  tokenList.appendChild(tokenItem);
2674
2661
  });
2675
- }
2662
+ },
2676
2663
 
2677
2664
  _effectiveAllowedTokens() {
2678
2665
  const keyTokens =
@@ -2682,17 +2669,64 @@ class KwesPayWidget {
2682
2669
  if (!keyTokens) return configTokens;
2683
2670
  if (!configTokens) return keyTokens;
2684
2671
  return keyTokens.filter((t) => configTokens.includes(t));
2685
- }
2672
+ },
2686
2673
 
2687
- // ── Wallet connection flow ───────────────────────────────────────────────────
2674
+ async _handleNetworkSelection(key, network) {
2675
+ this.state.selectedNetwork = key;
2676
+ this.state.selectedNetworkName = network.name;
2677
+ this.state.selectedChainId = network.chainId;
2678
+ this.state.selectedRpcUrl = network.rpcUrl;
2679
+ this.state.selectedContractAddress = network.contractAddress;
2680
+ this.state.selectedToken = null;
2681
+ this.state.selectedTokenConfig = null;
2688
2682
 
2683
+ const nameEl = document.getElementById("kwespay-selectedNetworkName");
2684
+ if (nameEl)
2685
+ nameEl.textContent = `${network.name} ${
2686
+ network.type === "mainnet" ? "Mainnet" : "Testnet"
2687
+ }`;
2688
+
2689
+ const iconEl = document.getElementById("kwespay-selectedNetworkIcon");
2690
+ if (iconEl)
2691
+ iconEl.innerHTML = `<img src="${network.logo}" alt="${network.name}" />`;
2692
+
2693
+ const continueBtn = document.getElementById(
2694
+ "kwespay-continueToWalletConnect"
2695
+ );
2696
+ if (continueBtn) continueBtn.disabled = true;
2697
+
2698
+ this._renderTokenList();
2699
+ this._goToStep(2);
2700
+ },
2701
+
2702
+ _handleTokenSelection(token) {
2703
+ document
2704
+ .querySelectorAll("#kwespay-tokenList .token-item")
2705
+ .forEach((item) => item.classList.remove("selected"));
2706
+ document
2707
+ .querySelector(
2708
+ `#kwespay-tokenList .token-item[data-token-symbol="${token.symbol}"]`
2709
+ )
2710
+ ?.classList.add("selected");
2711
+
2712
+ this.state.selectedToken = token.symbol;
2713
+ this.state.selectedTokenConfig = token;
2714
+
2715
+ const continueBtn = document.getElementById(
2716
+ "kwespay-continueToWalletConnect"
2717
+ );
2718
+ if (continueBtn) continueBtn.disabled = false;
2719
+ },
2720
+ };
2721
+
2722
+ const WalletMethods = {
2689
2723
  async _handleWalletConnection() {
2690
2724
  if (!this.state.selectedToken || !this.state.selectedTokenConfig) {
2691
2725
  alert("Please select a token first");
2692
2726
  return;
2693
2727
  }
2694
2728
  this._renderWalletPickerStep();
2695
- }
2729
+ },
2696
2730
 
2697
2731
  _renderWalletPickerStep() {
2698
2732
  this._removeCustomStep("kwespay-step-wallet-picker");
@@ -2790,7 +2824,7 @@ class KwesPayWidget {
2790
2824
  list.appendChild(item);
2791
2825
  });
2792
2826
  });
2793
- }
2827
+ },
2794
2828
 
2795
2829
  async _connectInjectedProvider(index) {
2796
2830
  try {
@@ -2805,7 +2839,7 @@ class KwesPayWidget {
2805
2839
  );
2806
2840
  dispatchWidgetEvent("walletConnectionError", { error: err.message });
2807
2841
  }
2808
- }
2842
+ },
2809
2843
 
2810
2844
  async _startWalletConnect() {
2811
2845
  this._renderWCStep();
@@ -2821,7 +2855,7 @@ class KwesPayWidget {
2821
2855
  this._renderWCMobileStep();
2822
2856
  }
2823
2857
 
2824
- // Pass the target chain so MetaMask starts the WC session on the right
2858
+ // Pass the target chain so the wallet starts the WC session on the right
2825
2859
  // network — without this it defaults to its last-used WC chain (Mainnet).
2826
2860
  await this.walletService.connectWalletConnect(this.state.selectedChainId);
2827
2861
  } catch (err) {
@@ -2836,9 +2870,7 @@ class KwesPayWidget {
2836
2870
  err.message || "WalletConnect failed."
2837
2871
  );
2838
2872
  }
2839
- }
2840
-
2841
- // ── WalletConnect UI ─────────────────────────────────────────────────────────
2873
+ },
2842
2874
 
2843
2875
  _renderWCStep() {
2844
2876
  this._removeCustomStep("kwespay-step-wc");
@@ -2871,7 +2903,7 @@ class KwesPayWidget {
2871
2903
  });
2872
2904
 
2873
2905
  this._goToStep("wc");
2874
- }
2906
+ },
2875
2907
 
2876
2908
  _wcDesktopHTML() {
2877
2909
  return `
@@ -2893,7 +2925,7 @@ class KwesPayWidget {
2893
2925
  <p style="font-size:11px;color:var(--kp-muted);font-family:var(--kp-mono);text-align:center">Waiting for wallet connection…</p>
2894
2926
  </div>
2895
2927
  `;
2896
- }
2928
+ },
2897
2929
 
2898
2930
  _wcMobileHTML() {
2899
2931
  const wallets = this.walletService.getMobileWallets();
@@ -2941,7 +2973,7 @@ class KwesPayWidget {
2941
2973
  </p>
2942
2974
  </div>
2943
2975
  `;
2944
- }
2976
+ },
2945
2977
 
2946
2978
  _renderWCMobileStep() {
2947
2979
  const list = document.getElementById("kwespay-mobile-wallet-list");
@@ -2965,7 +2997,7 @@ class KwesPayWidget {
2965
2997
  }
2966
2998
  });
2967
2999
  });
2968
- }
3000
+ },
2969
3001
 
2970
3002
  _onWCUri(uri) {
2971
3003
  this.state.wcUri = uri;
@@ -2982,7 +3014,7 @@ class KwesPayWidget {
2982
3014
  } else {
2983
3015
  this._renderQRCode(uri);
2984
3016
  }
2985
- }
3017
+ },
2986
3018
 
2987
3019
  _renderQRCode(uri) {
2988
3020
  const canvas = document.getElementById("kwespay-qr-canvas");
@@ -3017,18 +3049,18 @@ class KwesPayWidget {
3017
3049
  <p style="font-size:10px;color:#888;font-family:monospace">Use Copy URI button</p>
3018
3050
  </div>
3019
3051
  `;
3020
- }
3052
+ },
3021
3053
 
3022
3054
  _onWCConnected() {
3023
3055
  const connection = this.walletService._connectionResult();
3024
3056
  this._removeCustomStep("kwespay-step-wc");
3025
3057
  this._onWalletConnected(connection);
3026
- }
3058
+ },
3027
3059
 
3028
3060
  _onWCDisconnected() {
3029
3061
  this._removeCustomStep("kwespay-step-wc");
3030
3062
  this._goToStep(2);
3031
- }
3063
+ },
3032
3064
 
3033
3065
  async _onWalletConnected(connection) {
3034
3066
  const addressEl = document.getElementById("kwespay-connectedWalletAddress");
@@ -3045,7 +3077,7 @@ class KwesPayWidget {
3045
3077
  const proceedBtn = document.getElementById("kwespay-proceedToPayment");
3046
3078
 
3047
3079
  if (cryptoLine) {
3048
- cryptoLine.textContent = "Getting the best rate for you…";
3080
+ cryptoLine.textContent = "loading…";
3049
3081
  cryptoLine.classList.add("loading");
3050
3082
  }
3051
3083
  if (timerEl) timerEl.style.display = "none";
@@ -3067,83 +3099,39 @@ class KwesPayWidget {
3067
3099
  }
3068
3100
 
3069
3101
  dispatchWidgetEvent("walletConnected", { address: connection.address });
3070
- }
3071
-
3072
- // ── API key validation ───────────────────────────────────────────────────────
3073
-
3074
- async _validateAPIKey() {
3075
- try {
3076
- const validation = await this.paymentService.validateAPIKey();
3077
- if (validation.valid) {
3078
- this.state.vendorInfo = validation.vendorInfo;
3079
- this.state.keyAllowedNetworks = validation.allowedNetworks ?? null;
3080
- this.state.keyAllowedTokens = validation.allowedTokens ?? null;
3081
- this._goToStep(1);
3082
- dispatchWidgetEvent("apiKeyValidated", {
3083
- vendorInfo: this.state.vendorInfo,
3084
- });
3085
- } else {
3086
- this._goToStep(0.5);
3087
- dispatchWidgetEvent("apiKeyInvalid", {});
3088
- }
3089
- } catch (err) {
3090
- console.error("[KwesPayWidget] API key validation error:", err.message);
3091
- this._goToStep(0.5);
3092
- dispatchWidgetEvent("apiKeyError", { error: err.message });
3093
- }
3094
- }
3095
-
3096
- // ── Network / token selection ────────────────────────────────────────────────
3102
+ },
3103
+ };
3097
3104
 
3098
- async _handleNetworkSelection(key, network) {
3099
- this.state.selectedNetwork = key;
3100
- this.state.selectedNetworkName = network.name;
3101
- this.state.selectedChainId = network.chainId;
3102
- this.state.selectedRpcUrl = network.rpcUrl;
3103
- this.state.selectedContractAddress = network.contractAddress;
3104
- this.state.selectedToken = null;
3105
- this.state.selectedTokenConfig = null;
3105
+ const PLATFORM_FEE_BPS = 25;
3106
3106
 
3107
- const nameEl = document.getElementById("kwespay-selectedNetworkName");
3108
- if (nameEl)
3109
- nameEl.textContent = `${network.name} ${
3110
- network.type === "mainnet" ? "Mainnet" : "Testnet"
3111
- }`;
3107
+ function formatUnits(rawBigInt, decimals) {
3108
+ const divisor = BigInt(10 ** decimals);
3109
+ const whole = rawBigInt / divisor;
3110
+ const remainder = rawBigInt % divisor;
3112
3111
 
3113
- const iconEl = document.getElementById("kwespay-selectedNetworkIcon");
3114
- if (iconEl)
3115
- iconEl.innerHTML = `<img src="${network.logo}" alt="${network.name}" />`;
3112
+ if (remainder === 0n) return `${whole}`;
3116
3113
 
3117
- const continueBtn = document.getElementById(
3118
- "kwespay-continueToWalletConnect"
3119
- );
3120
- if (continueBtn) continueBtn.disabled = true;
3114
+ const fracFull = remainder.toString().padStart(decimals, "0");
3121
3115
 
3122
- this._renderTokenList();
3123
- this._goToStep(2);
3116
+ if (whole > 0n) {
3117
+ const frac = fracFull.slice(0, 4).replace(/0+$/, "");
3118
+ return frac ? `${whole}.${frac}` : `${whole}`;
3124
3119
  }
3125
3120
 
3126
- _handleTokenSelection(token) {
3127
- document
3128
- .querySelectorAll("#kwespay-tokenList .token-item")
3129
- .forEach((item) => item.classList.remove("selected"));
3130
- document
3131
- .querySelector(
3132
- `#kwespay-tokenList .token-item[data-token-symbol="${token.symbol}"]`
3133
- )
3134
- ?.classList.add("selected");
3135
-
3136
- this.state.selectedToken = token.symbol;
3137
- this.state.selectedTokenConfig = token;
3138
-
3139
- const continueBtn = document.getElementById(
3140
- "kwespay-continueToWalletConnect"
3141
- );
3142
- if (continueBtn) continueBtn.disabled = false;
3121
+ let firstSig = -1;
3122
+ for (let i = 0; i < fracFull.length; i++) {
3123
+ if (fracFull[i] !== "0") {
3124
+ firstSig = i;
3125
+ break;
3126
+ }
3143
3127
  }
3128
+ if (firstSig === -1) return `${whole}`;
3144
3129
 
3145
- // ── Quote / review step ──────────────────────────────────────────────────────
3130
+ const sigSlice = fracFull.slice(firstSig, firstSig + 4).replace(/0+$/, "");
3131
+ return `0.${fracFull.slice(0, firstSig) + sigSlice}`;
3132
+ }
3146
3133
 
3134
+ const QuoteMethods = {
3147
3135
  async _loadReviewStep() {
3148
3136
  this._clearQuoteTimer();
3149
3137
 
@@ -3155,7 +3143,7 @@ class KwesPayWidget {
3155
3143
  const proceedBtn = document.getElementById("kwespay-proceedToPayment");
3156
3144
 
3157
3145
  if (cryptoLine) {
3158
- cryptoLine.textContent = "Getting the best rate for you…";
3146
+ cryptoLine.textContent = "loading…";
3159
3147
  cryptoLine.classList.add("loading");
3160
3148
  }
3161
3149
  if (timerEl) timerEl.style.display = "none";
@@ -3200,7 +3188,7 @@ class KwesPayWidget {
3200
3188
  }
3201
3189
  if (proceedBtn) proceedBtn.disabled = true;
3202
3190
  }
3203
- }
3191
+ },
3204
3192
 
3205
3193
  _startQuoteTimer(expiresAt) {
3206
3194
  const timerEl = document.getElementById("kwespay-quoteTimer");
@@ -3221,6 +3209,7 @@ class KwesPayWidget {
3221
3209
  "kp-quote-timer" +
3222
3210
  (secs <= 30 ? " urgent" : "") +
3223
3211
  (secs <= 0 ? " expired" : "");
3212
+
3224
3213
  if (secs <= 0) {
3225
3214
  timerText.textContent = "Refreshing your rate…";
3226
3215
  const proceedBtn = document.getElementById("kwespay-proceedToPayment");
@@ -3229,19 +3218,20 @@ class KwesPayWidget {
3229
3218
  this._loadReviewStep();
3230
3219
  }
3231
3220
  };
3221
+
3232
3222
  update();
3233
3223
  this.state.quoteTimerInterval = setInterval(update, 1000);
3234
- }
3224
+ },
3235
3225
 
3236
3226
  _clearQuoteTimer() {
3237
3227
  if (this.state.quoteTimerInterval) {
3238
3228
  clearInterval(this.state.quoteTimerInterval);
3239
3229
  this.state.quoteTimerInterval = null;
3240
3230
  }
3241
- }
3242
-
3243
- // ── Payment processing ───────────────────────────────────────────────────────
3231
+ },
3232
+ };
3244
3233
 
3234
+ const PaymentMethods = {
3245
3235
  async _handlePaymentProcessing() {
3246
3236
  if (!this.state.currentPayload) {
3247
3237
  this._showError(
@@ -3270,9 +3260,7 @@ class KwesPayWidget {
3270
3260
 
3271
3261
  if (!provider) throw new Error("No wallet provider");
3272
3262
 
3273
- // ── Session liveness check ───────────────────────────────────────────────
3274
- // Must come first — a stale WC session throws on any RPC call, and we want
3275
- // a clean "reconnect" error rather than a cryptic provider error downstream.
3263
+
3276
3264
  const alive = await this.walletService.isSessionAlive();
3277
3265
  if (!alive) {
3278
3266
  console.error(
@@ -3286,22 +3274,17 @@ class KwesPayWidget {
3286
3274
  throw err;
3287
3275
  }
3288
3276
 
3289
- // ── Show mobile helper banner ────────────────────────────────────────────
3277
+
3290
3278
  if (isMobile) {
3291
3279
  document
3292
3280
  .getElementById("kwespay-mobileTransactionInstruction")
3293
3281
  ?.style.setProperty("display", "flex");
3294
3282
  }
3295
3283
 
3296
- // ── Chain validation ─────────────────────────────────────────────────────
3297
3284
  if (strictMobile) {
3298
- // On mobile WC, eth_chainId goes to the wallet via the WC relay and
3299
- // reflects the wallet's actual active chain — always call it live.
3300
- // Never rely on session namespace account ordering (MetaMask puts
3301
- // Mainnet first regardless of which chain the user is on).
3285
+
3302
3286
  await this._assertMobileChain(provider, targetChainId);
3303
3287
  } else {
3304
- // Desktop / injected — eth_chainId is reliable, switch if needed
3305
3288
  const rawChain = await provider.request({ method: "eth_chainId" });
3306
3289
  const currentChainId = parseInt(rawChain, 16);
3307
3290
 
@@ -3322,21 +3305,17 @@ class KwesPayWidget {
3322
3305
  this.state.selectedToken,
3323
3306
  this.state.selectedTokenConfig.decimals
3324
3307
  );
3325
- console.log("[KwesPay] Network switched");
3308
+ console.log("[KwesPay] Network switched");
3326
3309
  }
3327
3310
  }
3328
3311
 
3329
- // ── Open wallet BEFORE sending tx (mobile WC only) ──────────────────────
3330
- // We deep-link to the wallet before the JSON-RPC request lands so the
3331
- // approval prompt appears immediately without the user having to manually
3332
- // switch apps.
3312
+
3333
3313
  if (strictMobile) {
3334
3314
  setStatus(
3335
3315
  "Opening your wallet…",
3336
3316
  "Approve the payment in your wallet."
3337
3317
  );
3338
3318
  this.walletService._openWalletForApproval();
3339
- // Give the OS time to foreground the wallet app before the RPC lands
3340
3319
  await new Promise((r) => setTimeout(r, 700));
3341
3320
  } else {
3342
3321
  setStatus(
@@ -3345,19 +3324,18 @@ class KwesPayWidget {
3345
3324
  );
3346
3325
  }
3347
3326
 
3348
- // ── Send transaction ─────────────────────────────────────────────────────
3349
3327
  const receipt = await this.paymentService.createPayment({
3350
3328
  payload: this.state.currentPayload,
3351
3329
  walletProvider: provider,
3352
3330
  onStatusUpdate: setStatus,
3353
3331
  });
3354
3332
 
3355
- // ── Cleanup ──────────────────────────────────────────────────────────────
3333
+
3356
3334
  document
3357
3335
  .getElementById("kwespay-mobileTransactionInstruction")
3358
3336
  ?.style.setProperty("display", "none");
3359
3337
 
3360
- // ── Success UI ───────────────────────────────────────────────────────────
3338
+
3361
3339
  const decimals = this.state.selectedTokenConfig?.decimals ?? 6;
3362
3340
  const amountBig = BigInt(this.state.currentPayload.amountBaseUnits);
3363
3341
  const cryptoDisplay = `${formatUnits(amountBig, decimals)} ${
@@ -3420,7 +3398,7 @@ class KwesPayWidget {
3420
3398
  this._showError(title, message);
3421
3399
  dispatchWidgetEvent("paymentError", { error: message, errorType });
3422
3400
  }
3423
- }
3401
+ },
3424
3402
 
3425
3403
  /**
3426
3404
  * Confirm the wallet is on the target chain before sending a transaction.
@@ -3428,9 +3406,6 @@ class KwesPayWidget {
3428
3406
  * Throws WRONG_NETWORK if the chain never matches.
3429
3407
  *
3430
3408
  * MOBILE WC ONLY — never call this on desktop/injected.
3431
- *
3432
- * @param {object} provider
3433
- * @param {number} targetChainId
3434
3409
  */
3435
3410
  async _assertMobileChain(provider, targetChainId) {
3436
3411
  const MAX_ATTEMPTS = 3;
@@ -3464,25 +3439,14 @@ class KwesPayWidget {
3464
3439
  }
3465
3440
  }
3466
3441
 
3467
- // All attempts failed — wallet is on wrong chain
3468
3442
  const err = new Error(
3469
3443
  `Please switch to ${this.state.selectedNetworkName} in your wallet and try again.`
3470
3444
  );
3471
3445
  err.code = "WRONG_NETWORK";
3472
3446
  throw err;
3473
- }
3447
+ },
3448
+
3474
3449
 
3475
- /**
3476
- * Switch network for desktop/injected providers and poll until confirmed.
3477
- * MUST NOT be called in strictMobile mode (mobile WC doesn't support
3478
- * wallet_switchEthereumChain reliably).
3479
- *
3480
- * @param {number} chainId
3481
- * @param {string} networkName
3482
- * @param {string} rpcUrl
3483
- * @param {string} tokenSymbol
3484
- * @param {number} tokenDecimals
3485
- */
3486
3450
  async _switchNetworkSafe(
3487
3451
  chainId,
3488
3452
  networkName,
@@ -3490,7 +3454,6 @@ class KwesPayWidget {
3490
3454
  tokenSymbol,
3491
3455
  tokenDecimals
3492
3456
  ) {
3493
- // Capture before any async boundary — prevents `this` loss in callbacks
3494
3457
  const switchNetwork = this.walletService.switchNetwork;
3495
3458
  const provider = this.walletService.getProvider();
3496
3459
 
@@ -3513,12 +3476,11 @@ class KwesPayWidget {
3513
3476
 
3514
3477
  const targetHex = toHex(chainId);
3515
3478
 
3516
- // Check current chain — skip if already correct
3517
3479
  try {
3518
3480
  const currentHex = toHex(
3519
3481
  await provider.request({ method: "eth_chainId" })
3520
3482
  );
3521
- if (currentHex && currentHex === targetHex) return; // already correct, silent return
3483
+ if (currentHex && currentHex === targetHex) return;
3522
3484
  } catch (err) {
3523
3485
  console.warn(
3524
3486
  "[KwesPay] Could not read chainId before switch:",
@@ -3526,7 +3488,6 @@ class KwesPayWidget {
3526
3488
  );
3527
3489
  }
3528
3490
 
3529
- // Issue the switch
3530
3491
  try {
3531
3492
  await switchNetwork(
3532
3493
  chainId,
@@ -3572,44 +3533,67 @@ class KwesPayWidget {
3572
3533
  `Could not confirm network switch to ${networkName} after 15s. ` +
3573
3534
  `Please switch manually in your wallet and try again.`
3574
3535
  );
3575
- }
3536
+ },
3537
+ };
3538
+
3539
+ function resolveAcceptedTokens(input) {
3540
+ if (!input) return null;
3541
+ if (input === "stablecoins") return STABLECOIN_SYMBOLS;
3542
+ if (Array.isArray(input) && input.length)
3543
+ return input.map((t) => t.toUpperCase());
3544
+ return null;
3545
+ }
3576
3546
 
3577
- // ── Step navigation ──────────────────────────────────────────────────────────
3547
+ class KwesPayWidget {
3548
+ constructor(config) {
3549
+ if (!config.apiKey) throw new Error("[KwesPayWidget] apiKey is required");
3550
+ if (!config.vendorId)
3551
+ throw new Error("[KwesPayWidget] vendorId is required");
3552
+ if (!config.amount || parseFloat(config.amount) <= 0)
3553
+ throw new Error("[KwesPayWidget] Valid amount is required");
3578
3554
 
3579
- _goToStep(stepNumber) {
3580
- document
3581
- .querySelectorAll(".kwespay-container .step")
3582
- .forEach((s) => s.classList.remove("active", "exiting"));
3583
- this._activateStep(stepNumber);
3584
- this.state.currentStep = stepNumber;
3585
- }
3555
+ this.config = {
3556
+ apiKey: config.apiKey,
3557
+ vendorId: config.vendorId,
3558
+ amount: parseFloat(config.amount),
3559
+ currency: config.currency || DEFAULT_CONFIG.currency,
3560
+ graphqlEndpoint: DEFAULT_CONFIG.graphqlEndpoint,
3561
+ acceptedTokens: resolveAcceptedTokens(config.acceptedTokens),
3562
+ };
3586
3563
 
3587
- _activateStep(stepNumber) {
3588
- let targetStep;
3589
- if (stepNumber === 0.5)
3590
- targetStep = document.getElementById("kwespay-step0-invalid");
3591
- else if (typeof stepNumber === "string")
3592
- targetStep = document.getElementById(`kwespay-step-${stepNumber}`);
3593
- else targetStep = document.getElementById(`kwespay-step${stepNumber}`);
3564
+ if (!Object.values(SUPPORTED_CURRENCIES).includes(this.config.currency)) {
3565
+ throw new Error(
3566
+ `[KwesPayWidget] Unsupported currency: ${this.config.currency}`
3567
+ );
3568
+ }
3594
3569
 
3595
- if (targetStep) targetStep.classList.add("active");
3596
- else console.warn(`[KwesPayWidget] Step not found: ${stepNumber}`);
3597
- }
3570
+ this.walletService = new WalletService();
3571
+ this.paymentService = new PaymentService(
3572
+ this.config.apiKey,
3573
+ this.config.graphqlEndpoint
3574
+ );
3598
3575
 
3599
- _removeCustomStep(id) {
3600
- document.getElementById(id)?.remove();
3601
- }
3576
+ this.state = {
3577
+ isOpen: false,
3578
+ currentStep: 0,
3579
+ selectedNetwork: null,
3580
+ selectedNetworkName: "",
3581
+ selectedChainId: null,
3582
+ selectedRpcUrl: null,
3583
+ selectedContractAddress: null,
3584
+ selectedToken: null,
3585
+ selectedTokenConfig: null,
3586
+ vendorInfo: null,
3587
+ keyAllowedNetworks: null,
3588
+ keyAllowedTokens: null,
3589
+ currentPayload: null,
3590
+ quoteTimerInterval: null,
3591
+ wcUri: null,
3592
+ };
3602
3593
 
3603
- _showError(title, message) {
3604
- const titleEl = document.getElementById("kwespay-errorTitle");
3605
- const msgEl = document.getElementById("kwespay-errorMessage");
3606
- if (titleEl) titleEl.textContent = title;
3607
- if (msgEl) msgEl.textContent = message;
3608
- this._goToStep(6);
3594
+ this._init();
3609
3595
  }
3610
3596
 
3611
- // ── Public API ───────────────────────────────────────────────────────────────
3612
-
3613
3597
  async open() {
3614
3598
  const overlay = document.getElementById("kwespay-widget-overlay");
3615
3599
  const container = document.getElementById("kwespay-widget-container");
@@ -3685,21 +3669,6 @@ class KwesPayWidget {
3685
3669
  return { ...this.state, config: { ...this.config } };
3686
3670
  }
3687
3671
 
3688
- _reset() {
3689
- this._clearQuoteTimer();
3690
- this._removeCustomStep("kwespay-step-wallet-picker");
3691
- this._removeCustomStep("kwespay-step-wc");
3692
- this.state.selectedNetwork = null;
3693
- this.state.selectedNetworkName = "";
3694
- this.state.selectedChainId = null;
3695
- this.state.selectedRpcUrl = null;
3696
- this.state.selectedContractAddress = null;
3697
- this.state.selectedToken = null;
3698
- this.state.selectedTokenConfig = null;
3699
- this.state.currentPayload = null;
3700
- this.state.wcUri = null;
3701
- }
3702
-
3703
3672
  destroy() {
3704
3673
  this._clearQuoteTimer();
3705
3674
  document.body.classList.remove("kwespay-open");
@@ -3714,6 +3683,17 @@ class KwesPayWidget {
3714
3683
  }
3715
3684
  }
3716
3685
 
3686
+ Object.assign(
3687
+ KwesPayWidget.prototype,
3688
+ DomMethods,
3689
+ NavMethods,
3690
+ APIKeyMethods,
3691
+ NetworkMethods,
3692
+ WalletMethods,
3693
+ QuoteMethods,
3694
+ PaymentMethods
3695
+ );
3696
+
3717
3697
  /**
3718
3698
  * KwesPay Widget - Main entry point
3719
3699
  * @module @kwespay/widget