@kwespay/widget 1.0.6 → 1.0.8

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