@kwespay/widget 1.0.8 → 1.0.10

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
@@ -5,10 +5,17 @@ const SUPPORTED_CURRENCIES = {
5
5
  GHS: "GHS",
6
6
  };
7
7
 
8
- const STABLECOIN_SYMBOLS = ["USDT", "USDC", "DAI", "BUSD", "USDc"];
8
+ const STABLECOIN_SYMBOLS = [
9
+ "USDT",
10
+ "USDC",
11
+ "DAI",
12
+ "BUSD",
13
+ "USDc",
14
+ "MUSD",
15
+ ];
9
16
 
10
17
  const DEFAULT_CONFIG = {
11
- graphqlEndpoint: "https://api.kwespay.xyz/",
18
+ graphqlEndpoint: "https://api.testnet.kwespay.xyz/graphql",
12
19
  currency: SUPPORTED_CURRENCIES.USD,
13
20
  };
14
21
 
@@ -49,11 +56,29 @@ const NETWORK_CONFIGS = {
49
56
  explorer: "https://blockscout.lisk.com/tx/",
50
57
  type: "mainnet",
51
58
  },
59
+ mezo: {
60
+ name: "Mezo",
61
+ chainId: 31612,
62
+ rpcUrl: "https://rpc.mezo.org",
63
+ contractAddress: "",
64
+ logo: "https://arthuremma2.github.io/img-hosting/mezo.png",
65
+ explorer: "https://explorer.mezo.org/tx/",
66
+ type: "mainnet",
67
+ },
68
+ arbitrum: {
69
+ name: "Arbitrum",
70
+ chainId: 42161,
71
+ rpcUrl: "https://arb1.arbitrum.io/rpc",
72
+ contractAddress: "",
73
+ logo: "https://arthuremma2.github.io/img-hosting/arb44.svg",
74
+ explorer: "https://arbiscan.io/tx/",
75
+ type: "mainnet",
76
+ },
52
77
  sepolia: {
53
78
  name: "Sepolia",
54
79
  chainId: 11155111,
55
80
  rpcUrl: "https://rpc.sepolia.org",
56
- contractAddress: "0x39bE436D6A34d0990cb71c9cBD24a5361d85e00B",
81
+ contractAddress: "0xD9312df771aEf74a6748c0C46A706873C67F44C7",
57
82
  logo: "https://arthuremma2.github.io/img-hosting/ethereum-eth.svg",
58
83
  explorer: "https://sepolia.etherscan.io/tx/",
59
84
  type: "testnet",
@@ -62,7 +87,7 @@ const NETWORK_CONFIGS = {
62
87
  name: "Polygon Amoy",
63
88
  chainId: 80002,
64
89
  rpcUrl: "https://rpc-amoy.polygon.technology",
65
- contractAddress: "0xD31dF3eBd220Fd3e190A346F8927819295d28980",
90
+ contractAddress: "0xEb40935599d5D8ef39C1aAE38E7A1f6d9c89B3fF",
66
91
  logo: "https://arthuremma2.github.io/img-hosting/Polygon_Icon.png",
67
92
  explorer: "https://amoy.polygonscan.com/tx/",
68
93
  type: "testnet",
@@ -71,7 +96,7 @@ const NETWORK_CONFIGS = {
71
96
  name: "Base Sepolia",
72
97
  chainId: 84532,
73
98
  rpcUrl: "https://sepolia.base.org",
74
- contractAddress: "0x7515b1b1BcA33E7a9ccBd5E2b93771884654De77",
99
+ contractAddress: "0x3d7A6a7aD72374D2d3dca4e97053bAbFA6E49ec0",
75
100
  logo: "https://arthuremma2.github.io/img-hosting/base23.png",
76
101
  explorer: "https://sepolia.basescan.org/tx/",
77
102
  type: "testnet",
@@ -80,11 +105,29 @@ const NETWORK_CONFIGS = {
80
105
  name: "Lisk Sepolia",
81
106
  chainId: 4202,
82
107
  rpcUrl: "https://rpc.sepolia-api.lisk.com",
83
- contractAddress: "0xd04A78a998146EBAD04c2b68E020C06Dc3b3717f",
108
+ contractAddress: "0x3378B6074A9DA47Aef8b7C849aFcaF58b8D8134b",
84
109
  logo: "https://arthuremma2.github.io/img-hosting/liskt.png",
85
110
  explorer: "https://sepolia-blockscout.lisk.com/tx/",
86
111
  type: "testnet",
87
112
  },
113
+ mezoTestnet: {
114
+ name: "Mezo Testnet",
115
+ chainId: 31611,
116
+ rpcUrl: "https://rpc.test.mezo.org",
117
+ contractAddress: "0x67f3Df6B5BE714303F397104d8F2A3861b9E8b6d",
118
+ logo: "https://arthuremma2.github.io/img-hosting/mezo.png",
119
+ explorer: "https://explorer.test.mezo.org/tx/",
120
+ type: "testnet",
121
+ },
122
+ arbitrumSepolia: {
123
+ name: "Arbitrum Sepolia",
124
+ chainId: 421614,
125
+ rpcUrl: "https://sepolia-rollup.arbitrum.io/rpc",
126
+ contractAddress: "0xa430B2e0D1273464809f8541286058e90781DA9C",
127
+ logo: "https://arthuremma2.github.io/img-hosting/arb44.svg",
128
+ explorer: "https://sepolia.arbiscan.io/tx/",
129
+ type: "testnet",
130
+ },
88
131
  };
89
132
 
90
133
  const TOKEN_CONFIGS = {
@@ -116,6 +159,15 @@ const TOKEN_CONFIGS = {
116
159
  coingeckoId: "usd-coin",
117
160
  binanceSymbol: "USDCUSDT",
118
161
  },
162
+ {
163
+ symbol: "DAI",
164
+ name: "Dai Stablecoin",
165
+ icon: "https://move-flow.github.io/assets/dai-logo.svg",
166
+ address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
167
+ decimals: 18,
168
+ coingeckoId: "dai",
169
+ binanceSymbol: "DAIUSDT",
170
+ },
119
171
  ],
120
172
  polygon: [
121
173
  {
@@ -136,6 +188,15 @@ const TOKEN_CONFIGS = {
136
188
  coingeckoId: "tether",
137
189
  binanceSymbol: "USDTUSDT",
138
190
  },
191
+ {
192
+ symbol: "USDC",
193
+ name: "USD Coin",
194
+ icon: "https://move-flow.github.io/assets/usd-coin-usdc-logo.svg",
195
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
196
+ decimals: 6,
197
+ coingeckoId: "usd-coin",
198
+ binanceSymbol: "USDCUSDT",
199
+ },
139
200
  ],
140
201
  base: [
141
202
  {
@@ -156,38 +217,172 @@ const TOKEN_CONFIGS = {
156
217
  coingeckoId: "usd-coin",
157
218
  binanceSymbol: "USDCUSDT",
158
219
  },
220
+ {
221
+ symbol: "USDbC",
222
+ name: "USD Base Coin",
223
+ icon: "https://move-flow.github.io/assets/usd-coin-usdc-logo.svg",
224
+ address: "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
225
+ decimals: 6,
226
+ coingeckoId: "usd-coin",
227
+ binanceSymbol: "USDCUSDT",
228
+ },
159
229
  ],
160
230
  lisk: [
161
231
  {
162
232
  symbol: "ETH",
163
- name: "Lisk",
164
- icon: "https://arthuremma2.github.io/img-hosting/liskt.png",
233
+ name: "Ethereum",
234
+ icon: "https://arthuremma2.github.io/img-hosting/ethereum-eth.svg",
165
235
  address: "0x0000000000000000000000000000000000000000",
166
236
  decimals: 18,
167
237
  coingeckoId: "ethereum",
168
238
  binanceSymbol: "ETHUSDT",
169
239
  },
240
+ {
241
+ symbol: "LSK",
242
+ name: "Lisk",
243
+ icon: "https://arthuremma2.github.io/img-hosting/liskt.png",
244
+ address: "0x0000000000000000000000000000000000000000",
245
+ decimals: 18,
246
+ coingeckoId: "lisk",
247
+ binanceSymbol: "LSKUSDT",
248
+ },
170
249
  ],
171
- sepolia: [
250
+ mezo: [
251
+ // {
252
+ // symbol: "BTC",
253
+ // name: "Bitcoin",
254
+ // icon: "https://arthuremma2.github.io/img-hosting/btc.png",
255
+ // address: "0x0000000000000000000000000000000000000000",
256
+ // decimals: 18,
257
+ // coingeckoId: "bitcoin",
258
+ // binanceSymbol: "BTCUSDT",
259
+ // },
260
+ {
261
+ symbol: "MEZO",
262
+ name: "Mezo",
263
+ icon: "https://arthuremma2.github.io/img-hosting/mezo.png",
264
+ address: "0x0000000000000000000000000000000000000000",
265
+ decimals: 18,
266
+ coingeckoId: "mezo",
267
+ binanceSymbol: null,
268
+ },
269
+ {
270
+ symbol: "USDT",
271
+ name: "Mock USDT",
272
+ icon: "https://move-flow.github.io/assets/tether-usdt-logo.svg",
273
+ address: "0xd16c9c341Bc15B5db7E25881893CA5a3117bB9A5",
274
+ decimals: 6,
275
+ coingeckoId: "tether",
276
+ binanceSymbol: "USDTUSDT",
277
+ },
278
+ ],
279
+ arbitrum: [
172
280
  {
173
281
  symbol: "ETH",
174
- name: "Sepolia ETH",
282
+ name: "Ethereum",
175
283
  icon: "https://arthuremma2.github.io/img-hosting/ethereum-eth.svg",
176
284
  address: "0x0000000000000000000000000000000000000000",
177
285
  decimals: 18,
178
286
  coingeckoId: "ethereum",
179
287
  binanceSymbol: "ETHUSDT",
180
288
  },
289
+ {
290
+ symbol: "USDC",
291
+ name: "USD Coin",
292
+ icon: "https://move-flow.github.io/assets/usd-coin-usdc-logo.svg",
293
+ address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
294
+ decimals: 6,
295
+ coingeckoId: "usd-coin",
296
+ binanceSymbol: "USDCUSDT",
297
+ },
181
298
  {
182
299
  symbol: "USDT",
183
- name: "Mock USDT",
300
+ name: "Tether USD",
184
301
  icon: "https://move-flow.github.io/assets/tether-usdt-logo.svg",
185
- address: "0x261322E2378467dd1cF5ac60A66817223db68fA3",
302
+ address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
303
+ decimals: 6,
304
+ coingeckoId: "tether",
305
+ binanceSymbol: "USDTUSDT",
306
+ },
307
+ ],
308
+ mezoTestnet: [
309
+ // {
310
+ // symbol: "BTC",
311
+ // name: "Bitcoin (Testnet)",
312
+ // icon: "https://arthuremma2.github.io/img-hosting/btc.png",
313
+ // address: "0x0000000000000000000000000000000000000000",
314
+ // decimals: 18,
315
+ // coingeckoId: "bitcoin",
316
+ // binanceSymbol: "BTCUSDT",
317
+ // },
318
+ {
319
+ symbol: "Mezo",
320
+ name: "Mezo",
321
+ icon: "https://arthuremma2.github.io/img-hosting/mezo.png",
322
+ address: "0x7B7c000000000000000000000000000000000001",
186
323
  decimals: 18,
324
+ coingeckoId: "mezo",
325
+ binanceSymbol: "mezo",
326
+ },
327
+ {
328
+ symbol: "USDT",
329
+ name: "Mock USDT",
330
+ icon: "https://move-flow.github.io/assets/tether-usdt-logo.svg",
331
+ address: "0xd16c9c341Bc15B5db7E25881893CA5a3117bB9A5",
332
+ decimals: 6,
187
333
  coingeckoId: "tether",
188
334
  binanceSymbol: "USDTUSDT",
189
335
  },
190
336
  ],
337
+ liskTestnet: [
338
+ {
339
+ symbol: "ETH",
340
+ name: "Lisk Sepolia ETH",
341
+ icon: "https://arthuremma2.github.io/img-hosting/ethereum-eth.svg",
342
+ address: "0x0000000000000000000000000000000000000000",
343
+ decimals: 18,
344
+ coingeckoId: "ethereum",
345
+ binanceSymbol: "ETHUSDT",
346
+ },
347
+ {
348
+ symbol: "LSK",
349
+ name: "Lisk",
350
+ icon: "https://arthuremma2.github.io/img-hosting/liskt.png",
351
+ address: "0x8a21CF9Ba08Ae709D64Cb25AfAA951183EC9FF6D",
352
+ decimals: 18,
353
+ coingeckoId: "lisk",
354
+ binanceSymbol: "LSKUSDT",
355
+ },
356
+ ],
357
+ sepolia: [
358
+ {
359
+ symbol: "ETH",
360
+ name: "Sepolia ETH",
361
+ icon: "https://arthuremma2.github.io/img-hosting/ethereum-eth.svg",
362
+ address: "0x0000000000000000000000000000000000000000",
363
+ decimals: 18,
364
+ coingeckoId: "ethereum",
365
+ binanceSymbol: "ETHUSDT",
366
+ },
367
+ {
368
+ symbol: "MUSD",
369
+ name: "MUSD",
370
+ icon: "https://arthuremma2.github.io/img-hosting/MUSD.png",
371
+ address: "0xeB5a5d39dE4Ea42C2Aa6A57EcA2894376683bB8E",
372
+ decimals: 18,
373
+ coingeckoId: "",
374
+ binanceSymbol: "",
375
+ },
376
+ {
377
+ symbol: "USDC",
378
+ name: "Mock USDC",
379
+ icon: "https://move-flow.github.io/assets/usd-coin-usdc-logo.svg",
380
+ address: "0xeB5a5d39dE4Ea42C2Aa6A57EcA2894376683bB8E",
381
+ decimals: 6,
382
+ coingeckoId: "usd-coin",
383
+ binanceSymbol: "USDCUSDT",
384
+ },
385
+ ],
191
386
  polygonAmoy: [
192
387
  {
193
388
  symbol: "MATIC",
@@ -199,13 +394,13 @@ const TOKEN_CONFIGS = {
199
394
  binanceSymbol: "MATICUSDT",
200
395
  },
201
396
  {
202
- symbol: "USDc",
203
- name: "Mock USDT",
204
- icon: "https://move-flow.github.io/assets/tether-usdt-logo.svg",
397
+ symbol: "USDC",
398
+ name: "Mock USDC",
399
+ icon: "https://move-flow.github.io/assets/usd-coin-usdc-logo.svg",
205
400
  address: "0x8B0180f2101c8260d49339abfEe87927412494B4",
206
401
  decimals: 6,
207
- coingeckoId: "tether",
208
- binanceSymbol: "USDTUSDT",
402
+ coingeckoId: "usd-coin",
403
+ binanceSymbol: "USDCUSDT",
209
404
  },
210
405
  ],
211
406
  baseSepolia: [
@@ -220,7 +415,7 @@ const TOKEN_CONFIGS = {
220
415
  },
221
416
  {
222
417
  symbol: "USDC",
223
- name: "USD Coin",
418
+ name: "Mock USDC",
224
419
  icon: "https://move-flow.github.io/assets/usd-coin-usdc-logo.svg",
225
420
  address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
226
421
  decimals: 6,
@@ -228,24 +423,33 @@ const TOKEN_CONFIGS = {
228
423
  binanceSymbol: "USDCUSDT",
229
424
  },
230
425
  ],
231
- liskTestnet: [
426
+ arbitrumSepolia: [
232
427
  {
233
428
  symbol: "ETH",
234
- name: "Lisk Sepolia",
235
- icon: "https://arthuremma2.github.io/img-hosting/lisk-logo.png",
429
+ name: "Arbitrum Sepolia ETH",
430
+ icon: "https://arthuremma2.github.io/img-hosting/ethereum-eth.svg",
236
431
  address: "0x0000000000000000000000000000000000000000",
237
432
  decimals: 18,
238
433
  coingeckoId: "ethereum",
239
434
  binanceSymbol: "ETHUSDT",
240
435
  },
241
436
  {
242
- symbol: "LSK",
243
- name: "Lisk",
244
- icon: "https://arthuremma2.github.io/img-hosting/liskt.png",
245
- address: "0x8a21CF9Ba08Ae709D64Cb25AfAA951183EC9FF6D",
437
+ symbol: "USDC",
438
+ name: "Mock USDC",
439
+ icon: "https://move-flow.github.io/assets/usd-coin-usdc-logo.svg",
440
+ address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
441
+ decimals: 6,
442
+ coingeckoId: "usd-coin",
443
+ binanceSymbol: "USDCUSDT",
444
+ },
445
+ {
446
+ symbol: "ARB",
447
+ name: "Arbitrum (Testnet)",
448
+ icon: "https://arthuremma2.github.io/img-hosting/arb44.svg",
449
+ address: "0x15dd68a7a37d7edBf284b93Bc25d6b286d33fCc3",
246
450
  decimals: 18,
247
- coingeckoId: "LSK",
248
- binanceSymbol: "LSK",
451
+ coingeckoId: "arbitrum",
452
+ binanceSymbol: "ARBUSDT",
249
453
  },
250
454
  ],
251
455
  };
@@ -809,36 +1013,36 @@ class PaymentService {
809
1013
  constructor(apiKey, graphqlEndpoint) {
810
1014
  this.apiKey = apiKey;
811
1015
  this.graphqlEndpoint = graphqlEndpoint;
1016
+ this._graphqlEndpoint = graphqlEndpoint;
1017
+ this._apiKey = apiKey;
812
1018
  this.client = null;
813
-
814
- console.log("[KwesPay] PaymentService initialized", {
815
- apiKey: this.apiKey?.slice(0, 6) + "...",
816
- });
817
1019
  }
818
1020
 
819
1021
  async _initClient() {
820
1022
  if (this.client) return;
821
-
822
- console.log("[KwesPay] Initializing SDK client...");
823
-
824
- const { KwesPayClient } = await import('./index-338l55OY.js');
825
-
1023
+ const { KwesPayClient } = await import('./index-BDgXWGKX.js');
826
1024
  this.client = new KwesPayClient({ apiKey: this.apiKey });
1025
+ }
827
1026
 
828
- console.log("[KwesPay] SDK client ready");
1027
+ async _rawGql(query, variables) {
1028
+ const res = await fetch(this._graphqlEndpoint, {
1029
+ method: "POST",
1030
+ headers: {
1031
+ "Content-Type": "application/json",
1032
+ "X-API-Key": this._apiKey,
1033
+ },
1034
+ body: JSON.stringify({ query, variables }),
1035
+ });
1036
+ return res.json();
829
1037
  }
830
1038
 
831
1039
  async validateAPIKey() {
832
1040
  try {
833
1041
  await this._initClient();
834
-
835
1042
  const result = await this.client.validateKey();
836
-
837
1043
  if (!result.isValid) {
838
- console.error("[KwesPay] Invalid API key:", result.error);
839
1044
  return { valid: false, error: result.error ?? "Invalid access key" };
840
1045
  }
841
-
842
1046
  return {
843
1047
  valid: true,
844
1048
  keyId: result.keyId,
@@ -855,94 +1059,94 @@ class PaymentService {
855
1059
 
856
1060
  async getQuote(params) {
857
1061
  await this._initClient();
858
-
859
- console.log("[KwesPay] Requesting quote...", params);
860
-
861
- const quote = await this.client.quote({
862
- vendorIdentifier: params.vendorId,
1062
+ return this.client.getQuote({
1063
+ vendorIdentifier: params.vendorIdentifier,
863
1064
  fiatAmount: params.fiatAmount,
864
1065
  fiatCurrency: params.fiatCurrency || "USD",
865
1066
  cryptoCurrency: params.cryptoCurrency,
866
1067
  network: params.network,
867
- payerWalletAddress: params.payerWalletAddress,
868
1068
  });
869
-
870
- console.log("[KwesPay] Quote received:", quote);
871
-
872
- return quote;
873
1069
  }
874
1070
 
875
1071
  async createPayment({ payload, walletProvider, onStatusUpdate }) {
876
1072
  await this._initClient();
877
1073
 
878
- console.log("[KwesPay] 💳 createPayment called", {
879
- payloadKeys: payload ? Object.keys(payload) : null,
880
- amountBaseUnits: payload?.amountBaseUnits,
881
- contractAddress: payload?.contractAddress,
882
- paymentId: payload?.paymentId,
883
- vendorAddress: payload?.vendorAddress,
884
- expiresAt: payload?.expiresAt,
885
- providerType: walletProvider?.constructor?.name,
886
- });
887
-
888
- try {
889
- const result = await this.client.pay({
890
- provider: walletProvider,
891
- payload,
892
- onStatus: (status) => {
893
- console.log("[KwesPay] 📋 Payment status update:", status);
894
- onStatusUpdate?.(status);
895
- },
896
- });
1074
+ const accounts = await walletProvider.request({ method: "eth_accounts" });
1075
+ const payerWalletAddress = accounts[0];
1076
+
1077
+ // Call createTransaction directly so we can request the deadline field,
1078
+ // which the SDK's GQL_CREATE_TRANSACTION query does not yet include.
1079
+ const rawTx = await this._rawGql(
1080
+ `mutation CreateTransaction($input: CreateTransactionInput!) {
1081
+ createTransaction(input: $input) {
1082
+ success
1083
+ message
1084
+ paymentIdBytes32
1085
+ backendSignature
1086
+ tokenAddress
1087
+ amountBaseUnits
1088
+ chainId
1089
+ deadline
1090
+ expiresAt
1091
+ transaction {
1092
+ transactionReference
1093
+ transactionStatus
1094
+ }
1095
+ }
1096
+ }`,
1097
+ { input: { quoteId: payload.quoteId, payerWalletAddress } }
1098
+ );
897
1099
 
898
- console.log("[KwesPay] Payment completed:", result);
1100
+ const ct = rawTx?.data?.createTransaction;
899
1101
 
900
- return {
901
- hash: result.txHash,
902
- blockNumber: result.blockNumber,
903
- transactionReference: result.transactionReference,
904
- paymentIdBytes32: result.paymentIdBytes32,
905
- };
906
- } catch (err) {
907
- console.error("[KwesPay] ❌ createPayment error:", {
908
- message: err?.message,
909
- code: err?.code,
910
- reason: err?.reason,
911
- data: err?.data,
912
- transaction: err?.transaction
913
- ? {
914
- to: err.transaction?.to,
915
- from: err.transaction?.from,
916
- value: err.transaction?.value?.toString(),
917
- gasLimit: err.transaction?.gasLimit?.toString(),
918
- data: err.transaction?.data,
919
- }
920
- : undefined,
921
- receipt: err?.receipt
922
- ? {
923
- status: err.receipt?.status,
924
- gasUsed: err.receipt?.gasUsed?.toString(),
925
- blockNumber: err.receipt?.blockNumber,
926
- transactionHash: err.receipt?.transactionHash,
927
- }
928
- : undefined,
929
- stack: err?.stack,
930
- raw: err,
931
- });
932
- throw err;
1102
+ if (!ct?.success) {
1103
+ throw new Error(ct?.message ?? "Transaction creation failed");
933
1104
  }
934
- }
935
1105
 
936
- async getTransactionStatus(transactionReference) {
937
- await this._initClient();
1106
+ if (!ct.deadline) {
1107
+ throw new Error("Backend did not return a deadline");
1108
+ }
938
1109
 
939
- console.log("[KwesPay] Fetching transaction status:", transactionReference);
1110
+ // Compute totalBaseUnits locally — mirrors the contract formula:
1111
+ // fee = (amount * 50) / 10000, total = amount + fee
1112
+ const PLATFORM_FEE_BPS = 50n;
1113
+ const amountBig = BigInt(ct.amountBaseUnits);
1114
+ const feeBig = (amountBig * PLATFORM_FEE_BPS) / 10000n;
1115
+ const totalBig = amountBig + feeBig;
1116
+
1117
+ // Build the complete TransactionPayload the SDK's pay() expects.
1118
+ const txPayload = {
1119
+ paymentIdBytes32: ct.paymentIdBytes32,
1120
+ backendSignature: ct.backendSignature,
1121
+ tokenAddress: ct.tokenAddress,
1122
+ amountBaseUnits: ct.amountBaseUnits,
1123
+ totalBaseUnits: totalBig.toString(),
1124
+ chainId: ct.chainId,
1125
+ deadline: ct.deadline,
1126
+ expiresAt: ct.expiresAt,
1127
+ transactionReference: ct.transaction.transactionReference,
1128
+ transactionStatus: ct.transaction.transactionStatus,
1129
+ network: payload.network,
1130
+ vendorIdentifier: payload.vendorIdentifier,
1131
+ };
940
1132
 
941
- const status = await this.client.getTransactionStatus(transactionReference);
1133
+ const result = await this.client.pay({
1134
+ provider: walletProvider,
1135
+ payload: txPayload,
1136
+ onStatus: (title, detail) => onStatusUpdate?.(title, detail),
1137
+ });
942
1138
 
943
- console.log("[KwesPay] Transaction status:", status);
1139
+ return {
1140
+ hash: result.txHash,
1141
+ blockNumber: result.blockNumber,
1142
+ transactionReference: result.transactionReference,
1143
+ paymentIdBytes32: result.paymentIdBytes32,
1144
+ };
1145
+ }
944
1146
 
945
- return status;
1147
+ async getTransactionStatus(transactionReference) {
1148
+ await this._initClient();
1149
+ return this.client.getTransactionStatus(transactionReference);
946
1150
  }
947
1151
 
948
1152
  async pollTransactionStatus(
@@ -950,28 +1154,13 @@ class PaymentService {
950
1154
  { onStatus, intervalMs = 4000, maxAttempts = 60 } = {}
951
1155
  ) {
952
1156
  await this._initClient();
953
-
954
- console.log("[KwesPay] Starting polling...", {
955
- transactionReference,
956
- intervalMs,
957
- });
958
-
959
1157
  let attempts = 0;
960
-
961
1158
  return new Promise((resolve, reject) => {
962
1159
  const id = setInterval(async () => {
963
1160
  attempts++;
964
-
965
1161
  try {
966
1162
  const status = await this.getTransactionStatus(transactionReference);
967
-
968
- console.log(
969
- `[KwesPay] Poll attempt ${attempts}:`,
970
- status.transactionStatus
971
- );
972
-
973
1163
  onStatus?.(status.transactionStatus);
974
-
975
1164
  const terminal = [
976
1165
  "completed",
977
1166
  "failed",
@@ -980,22 +1169,14 @@ class PaymentService {
980
1169
  "overpaid",
981
1170
  "refunded",
982
1171
  ];
983
-
984
1172
  if (terminal.includes(status.transactionStatus)) {
985
- console.log(
986
- "[KwesPay] Final status reached:",
987
- status.transactionStatus
988
- );
989
1173
  clearInterval(id);
990
1174
  resolve(status);
991
1175
  } else if (attempts >= maxAttempts) {
992
- console.error("[KwesPay] Polling timeout");
993
1176
  clearInterval(id);
994
1177
  reject(new Error("Transaction status polling timed out."));
995
1178
  }
996
1179
  } catch (err) {
997
- console.error("[KwesPay] Polling error:", err);
998
-
999
1180
  if (attempts >= maxAttempts) {
1000
1181
  clearInterval(id);
1001
1182
  reject(err);
@@ -1026,6 +1207,18 @@ function truncateHash(hash, startChars = 10, endChars = 8) {
1026
1207
  return `${hash.slice(0, startChars)}...${hash.slice(-endChars)}`;
1027
1208
  }
1028
1209
 
1210
+ function formatCryptoAmount(amount, symbol = "") {
1211
+ const num = typeof amount === "string" ? parseFloat(amount) : amount;
1212
+ if (isNaN(num) || num === 0) return symbol ? `0 ${symbol}` : "0";
1213
+
1214
+ const magnitude = Math.floor(Math.log10(Math.abs(num)));
1215
+ const decimalPlaces = Math.max(0, 5 - magnitude);
1216
+ const capped = Math.min(decimalPlaces, 8);
1217
+ const formatted = num.toFixed(capped).replace(/\.?0+$/, "");
1218
+
1219
+ return symbol ? `${formatted} ${symbol}` : formatted;
1220
+ }
1221
+
1029
1222
  function getErrorType(error) {
1030
1223
  const msg = error?.message ?? "";
1031
1224
  if (
@@ -1090,20 +1283,20 @@ const WIDGET_STYLES = `
1090
1283
  * { margin: 0; padding: 0; box-sizing: border-box; }
1091
1284
 
1092
1285
  :root {
1093
- --kp-bg: #0a0a0f;
1094
- --kp-surface: #0f0f18;
1095
- --kp-surface-2: #16161f;
1096
- --kp-border: rgba(255,255,255,0.06);
1286
+ --kp-bg: #0a0a0f;
1287
+ --kp-surface: #0f0f18;
1288
+ --kp-surface-2: #16161f;
1289
+ --kp-border: rgba(255,255,255,0.06);
1097
1290
  --kp-border-active: rgba(99,102,241,0.5);
1098
- --kp-accent: #6366f1;
1099
- --kp-accent-dim: rgba(99,102,241,0.1);
1100
- --kp-accent-glow: rgba(99,102,241,0.25);
1101
- --kp-green: #10b981;
1102
- --kp-red: #f43f5e;
1103
- --kp-text: #f1f0ff;
1104
- --kp-muted: #6b6a80;
1105
- --kp-mono: 'Inter', monospace;
1106
- --kp-sans: 'Inter', sans-serif;
1291
+ --kp-accent: #6366f1;
1292
+ --kp-accent-dim: rgba(99,102,241,0.1);
1293
+ --kp-accent-glow: rgba(99,102,241,0.25);
1294
+ --kp-green: #10b981;
1295
+ --kp-red: #f43f5e;
1296
+ --kp-text: #f1f0ff;
1297
+ --kp-muted: #6b6a80;
1298
+ --kp-mono: 'Inter', monospace;
1299
+ --kp-sans: 'Inter', sans-serif;
1107
1300
  }
1108
1301
 
1109
1302
  body.kwespay-open {
@@ -1128,19 +1321,17 @@ const WIDGET_STYLES = `
1128
1321
 
1129
1322
  .kwespay-close-btn {
1130
1323
  position: absolute;
1131
- top: 20px; right: 20px;
1324
+ top: 12px; right: 12px;
1132
1325
  background: rgba(255,255,255,0.06);
1133
1326
  border: 1px solid var(--kp-border);
1134
1327
  color: var(--kp-muted);
1135
- width: 34px; height: 34px;
1136
- border-radius: 8px;
1328
+ width: 28px; height: 28px;
1329
+ border-radius: 7px;
1137
1330
  cursor: pointer;
1138
- display: flex;
1139
- align-items: center;
1140
- justify-content: center;
1141
- font-size: 18px;
1331
+ display: flex; align-items: center; justify-content: center;
1332
+ font-size: 16px;
1142
1333
  transition: all 0.15s;
1143
- z-index: 1000000;
1334
+ z-index: 10;
1144
1335
  }
1145
1336
 
1146
1337
  .kwespay-close-btn:hover {
@@ -1149,6 +1340,12 @@ const WIDGET_STYLES = `
1149
1340
  border-color: var(--kp-border-active);
1150
1341
  }
1151
1342
 
1343
+ .kwespay-steps-wrapper {
1344
+ position: relative;
1345
+ flex: 1;
1346
+ overflow: hidden;
1347
+ }
1348
+
1152
1349
  .material-symbols-outlined {
1153
1350
  font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
1154
1351
  }
@@ -1207,6 +1404,8 @@ const WIDGET_STYLES = `
1207
1404
  to { opacity: 0; }
1208
1405
  }
1209
1406
 
1407
+
1408
+
1210
1409
  .kp-topbar {
1211
1410
  display: flex;
1212
1411
  align-items: center;
@@ -1223,11 +1422,8 @@ const WIDGET_STYLES = `
1223
1422
  }
1224
1423
 
1225
1424
  .kp-back-btn {
1226
- display: flex;
1227
- align-items: center;
1228
- justify-content: center;
1229
- width: 28px;
1230
- height: 28px;
1425
+ display: flex; align-items: center; justify-content: center;
1426
+ width: 28px; height: 28px;
1231
1427
  border-radius: 7px;
1232
1428
  background: rgba(255,255,255,0.04);
1233
1429
  border: 1px solid var(--kp-border);
@@ -1252,11 +1448,12 @@ const WIDGET_STYLES = `
1252
1448
  background: var(--kp-accent);
1253
1449
  box-shadow: 0 0 8px var(--kp-accent-glow);
1254
1450
  animation: kpPulse 2s ease infinite;
1451
+ flex-shrink: 0;
1255
1452
  }
1256
1453
 
1257
1454
  @keyframes kpPulse {
1258
1455
  0%, 100% { opacity: 1; }
1259
- 50% { opacity: 0.5; }
1456
+ 50% { opacity: 0.4; }
1260
1457
  }
1261
1458
 
1262
1459
  .kp-topbar-name {
@@ -1267,9 +1464,7 @@ const WIDGET_STYLES = `
1267
1464
  }
1268
1465
 
1269
1466
  .kp-topbar-secure {
1270
- display: flex;
1271
- align-items: center;
1272
- gap: 5px;
1467
+ display: flex; align-items: center; gap: 5px;
1273
1468
  font-family: var(--kp-mono);
1274
1469
  font-size: 10px;
1275
1470
  color: var(--kp-muted);
@@ -1277,10 +1472,8 @@ const WIDGET_STYLES = `
1277
1472
  font-weight: 500;
1278
1473
  }
1279
1474
 
1280
- .kp-topbar-secure .material-symbols-outlined {
1281
- font-size: 13px;
1282
- color: var(--kp-green);
1283
- }
1475
+ .kp-topbar-secure .material-symbols-outlined { font-size: 13px; color: var(--kp-green); }
1476
+
1284
1477
 
1285
1478
  .kp-amount-block {
1286
1479
  padding: 20px 20px 16px;
@@ -1327,6 +1520,8 @@ const WIDGET_STYLES = `
1327
1520
 
1328
1521
  .kp-amount-crypto.loading { opacity: 0.4; }
1329
1522
 
1523
+ /* ── Progress ───────────────────────────────────────────────────────────── */
1524
+
1330
1525
  .progress-section {
1331
1526
  padding: 14px 20px 12px;
1332
1527
  border-bottom: 1px solid var(--kp-border);
@@ -1334,9 +1529,7 @@ const WIDGET_STYLES = `
1334
1529
  }
1335
1530
 
1336
1531
  .progress-info {
1337
- display: flex;
1338
- align-items: center;
1339
- justify-content: space-between;
1532
+ display: flex; align-items: center; justify-content: space-between;
1340
1533
  margin-bottom: 8px;
1341
1534
  }
1342
1535
 
@@ -1359,8 +1552,7 @@ const WIDGET_STYLES = `
1359
1552
  .progress-bars { display: flex; width: 100%; gap: 5px; }
1360
1553
 
1361
1554
  .progress-bar {
1362
- height: 2px;
1363
- flex: 1;
1555
+ height: 2px; flex: 1;
1364
1556
  border-radius: 999px;
1365
1557
  background: var(--kp-surface-2);
1366
1558
  transition: background 0.3s ease;
@@ -1371,6 +1563,8 @@ const WIDGET_STYLES = `
1371
1563
  box-shadow: 0 0 8px var(--kp-accent-glow);
1372
1564
  }
1373
1565
 
1566
+ /* ── Scrollable content ─────────────────────────────────────────────────── */
1567
+
1374
1568
  .section-hint {
1375
1569
  font-size: 12px;
1376
1570
  color: var(--kp-muted);
@@ -1394,10 +1588,10 @@ const WIDGET_STYLES = `
1394
1588
  border-radius: 10px;
1395
1589
  }
1396
1590
 
1591
+ /* ── Network list ───────────────────────────────────────────────────────── */
1592
+
1397
1593
  .item-list {
1398
- display: flex;
1399
- flex-direction: column;
1400
- gap: 6px;
1594
+ display: flex; flex-direction: column; gap: 6px;
1401
1595
  margin-bottom: 6px;
1402
1596
  }
1403
1597
 
@@ -1416,9 +1610,7 @@ const WIDGET_STYLES = `
1416
1610
  }
1417
1611
 
1418
1612
  .list-item {
1419
- display: flex;
1420
- align-items: center;
1421
- gap: 12px;
1613
+ display: flex; align-items: center; gap: 12px;
1422
1614
  background: var(--kp-surface);
1423
1615
  padding: 12px 14px;
1424
1616
  border-radius: 12px;
@@ -1443,15 +1635,11 @@ const WIDGET_STYLES = `
1443
1635
  flex-shrink: 0;
1444
1636
  }
1445
1637
 
1446
- .item-icon img { width: 22px; height: 22px; object-fit: contain; }
1638
+ .item-icon img { width: 28px; height: 28px; object-fit: contain; }
1447
1639
  .item-info { display: flex; flex-direction: column; flex: 1; }
1448
1640
  .item-name-row { display: flex; align-items: center; gap: 7px; }
1449
1641
 
1450
- .item-name {
1451
- font-size: 13px;
1452
- font-weight: 600;
1453
- color: var(--kp-text);
1454
- }
1642
+ .item-name { font-size: 13px; font-weight: 600; color: var(--kp-text); }
1455
1643
 
1456
1644
  .item-badge {
1457
1645
  padding: 1px 6px;
@@ -1479,10 +1667,10 @@ const WIDGET_STYLES = `
1479
1667
 
1480
1668
  .item-chevron { color: var(--kp-muted); font-size: 16px; }
1481
1669
 
1670
+ /* ── Token list ─────────────────────────────────────────────────────────── */
1671
+
1482
1672
  .token-item {
1483
- display: flex;
1484
- align-items: center;
1485
- gap: 12px;
1673
+ display: flex; align-items: center; gap: 12px;
1486
1674
  padding: 13px 14px;
1487
1675
  cursor: pointer;
1488
1676
  transition: all 0.15s;
@@ -1495,14 +1683,24 @@ const WIDGET_STYLES = `
1495
1683
  }
1496
1684
 
1497
1685
  .token-item:last-child { margin-bottom: 0; }
1498
- .token-item:hover { border-color: var(--kp-border-active); background: var(--kp-accent-dim); }
1499
- .token-item.selected { background: var(--kp-accent-dim); border-color: var(--kp-accent); }
1686
+
1687
+ .token-item:hover {
1688
+ border-color: var(--kp-border-active);
1689
+ background: var(--kp-accent-dim);
1690
+ }
1691
+
1692
+ .token-item.selected {
1693
+ background: var(--kp-accent-dim);
1694
+ border-color: var(--kp-accent);
1695
+ }
1696
+
1500
1697
  .token-item.selected::after {
1501
1698
  content: '';
1502
1699
  position: absolute; inset: 0;
1503
1700
  background: linear-gradient(90deg, rgba(99,102,241,0.08) 0%, transparent 100%);
1504
1701
  pointer-events: none;
1505
1702
  }
1703
+
1506
1704
  .token-item.selected .token-symbol { color: var(--kp-accent); }
1507
1705
 
1508
1706
  .token-left { display: flex; align-items: center; gap: 12px; flex: 1; }
@@ -1512,11 +1710,8 @@ const WIDGET_STYLES = `
1512
1710
  border-radius: 10px;
1513
1711
  background: var(--kp-surface-2);
1514
1712
  border: 1px solid var(--kp-border);
1515
- overflow: hidden;
1516
- flex-shrink: 0;
1517
- display: flex;
1518
- align-items: center;
1519
- justify-content: center;
1713
+ overflow: hidden; flex-shrink: 0;
1714
+ display: flex; align-items: center; justify-content: center;
1520
1715
  }
1521
1716
 
1522
1717
  .token-icon img { width: 22px; height: 22px; object-fit: contain; }
@@ -1542,6 +1737,8 @@ const WIDGET_STYLES = `
1542
1737
  .token-chevron { color: var(--kp-muted); font-size: 16px; flex-shrink: 0; }
1543
1738
  #kwespay-tokenList { display: flex; flex-direction: column; }
1544
1739
 
1740
+ /* ── Buttons ────────────────────────────────────────────────────────────── */
1741
+
1545
1742
  .bottom-action {
1546
1743
  padding: 12px 20px 16px;
1547
1744
  background: var(--kp-bg);
@@ -1583,6 +1780,7 @@ const WIDGET_STYLES = `
1583
1780
  }
1584
1781
 
1585
1782
  .action-btn.secondary::before { display: none; }
1783
+
1586
1784
  .action-btn.secondary:hover {
1587
1785
  background: var(--kp-surface-2);
1588
1786
  border-color: var(--kp-border-active);
@@ -1592,14 +1790,13 @@ const WIDGET_STYLES = `
1592
1790
 
1593
1791
  .action-btn:disabled { opacity: 0.3; cursor: not-allowed; box-shadow: none; }
1594
1792
 
1793
+ /* ── Footer ─────────────────────────────────────────────────────────────── */
1794
+
1595
1795
  .kp-footer {
1596
1796
  padding: 10px 20px 12px;
1597
1797
  background: var(--kp-bg);
1598
1798
  border-top: 1px solid var(--kp-border);
1599
- display: flex;
1600
- align-items: center;
1601
- justify-content: center;
1602
- gap: 6px;
1799
+ display: flex; align-items: center; justify-content: center; gap: 6px;
1603
1800
  flex-shrink: 0;
1604
1801
  }
1605
1802
 
@@ -1615,11 +1812,11 @@ const WIDGET_STYLES = `
1615
1812
 
1616
1813
  .kp-footer-text span { color: var(--kp-text); font-weight: 500; }
1617
1814
 
1815
+ /* ── Spinner / loading ──────────────────────────────────────────────────── */
1816
+
1618
1817
  .loading-container {
1619
- display: flex;
1620
- flex-direction: column;
1621
- align-items: center;
1622
- justify-content: center;
1818
+ display: flex; flex-direction: column;
1819
+ align-items: center; justify-content: center;
1623
1820
  padding: 24px 20px;
1624
1821
  flex: 1;
1625
1822
  }
@@ -1649,6 +1846,21 @@ const WIDGET_STYLES = `
1649
1846
  animation: kpSpin 1.2s linear infinite reverse;
1650
1847
  }
1651
1848
 
1849
+ .kp-spin-soft.spinner-ring {
1850
+ border-top-color: rgba(16,185,129,0.55);
1851
+ animation-duration: 1.6s;
1852
+ }
1853
+
1854
+ .kp-spin-soft.spinner-ring-2 {
1855
+ border-bottom-color: rgba(16,185,129,0.18);
1856
+ animation-duration: 2.2s;
1857
+ }
1858
+
1859
+ .kp-spinner-icon-green {
1860
+ background: rgba(16,185,129,0.08) !important;
1861
+ border-color: rgba(16,185,129,0.15) !important;
1862
+ }
1863
+
1652
1864
  @keyframes kpSpin { to { transform: rotate(360deg); } }
1653
1865
 
1654
1866
  .spinner-icon {
@@ -1682,6 +1894,39 @@ const WIDGET_STYLES = `
1682
1894
  font-weight: 400;
1683
1895
  }
1684
1896
 
1897
+ /* ── Processing badges ──────────────────────────────────────────────────── */
1898
+
1899
+ .kp-onchain-badge {
1900
+ display: inline-flex; align-items: center; gap: 5px;
1901
+ margin-top: 14px;
1902
+ padding: 6px 14px;
1903
+ border-radius: 20px;
1904
+ background: rgba(16,185,129,0.08);
1905
+ border: 1px solid rgba(16,185,129,0.18);
1906
+ font-family: var(--kp-mono);
1907
+ font-size: 10px;
1908
+ color: var(--kp-green);
1909
+ letter-spacing: 0.04em;
1910
+ font-weight: 500;
1911
+ }
1912
+
1913
+ .kp-onchain-badge .material-symbols-outlined { font-size: 12px; color: var(--kp-green); }
1914
+
1915
+ .kp-confirmed-bar {
1916
+ display: flex; align-items: center; justify-content: center; gap: 6px;
1917
+ padding: 8px 20px;
1918
+ background: rgba(16,185,129,0.06);
1919
+ border-bottom: 1px solid rgba(16,185,129,0.12);
1920
+ font-family: var(--kp-mono);
1921
+ font-size: 10px;
1922
+ color: var(--kp-green);
1923
+ letter-spacing: 0.04em;
1924
+ font-weight: 500;
1925
+ flex-shrink: 0;
1926
+ }
1927
+
1928
+ /* ── Success icons ──────────────────────────────────────────────────────── */
1929
+
1685
1930
  .success-icon {
1686
1931
  width: 64px; height: 64px;
1687
1932
  border-radius: 16px;
@@ -1693,6 +1938,8 @@ const WIDGET_STYLES = `
1693
1938
 
1694
1939
  .success-icon .material-symbols-outlined { font-size: 36px; color: var(--kp-green); }
1695
1940
 
1941
+ /* ── Error icon + hint ──────────────────────────────────────────────────── */
1942
+
1696
1943
  .error-icon {
1697
1944
  width: 64px; height: 64px;
1698
1945
  border-radius: 16px;
@@ -1704,12 +1951,28 @@ const WIDGET_STYLES = `
1704
1951
 
1705
1952
  .error-icon .material-symbols-outlined { font-size: 36px; color: var(--kp-red); }
1706
1953
 
1954
+ /* Reassuring strip below the error message */
1955
+ .kp-error-hint {
1956
+ display: flex; align-items: flex-start; gap: 7px;
1957
+ margin-top: 16px;
1958
+ padding: 10px 14px;
1959
+ border-radius: 10px;
1960
+ background: rgba(255,255,255,0.03);
1961
+ border: 1px solid var(--kp-border);
1962
+ font-family: var(--kp-mono);
1963
+ font-size: 11px;
1964
+ color: var(--kp-muted);
1965
+ line-height: 1.55;
1966
+ max-width: 280px;
1967
+ text-align: left;
1968
+ }
1969
+
1970
+ /* ── Network status pill ────────────────────────────────────────────────── */
1971
+
1707
1972
  .network-status { padding: 14px 20px 0; }
1708
1973
 
1709
1974
  .status-card {
1710
- display: flex;
1711
- align-items: center;
1712
- gap: 10px;
1975
+ display: flex; align-items: center; gap: 10px;
1713
1976
  background: var(--kp-accent-dim);
1714
1977
  border: 1px solid rgba(99,102,241,0.2);
1715
1978
  border-radius: 10px;
@@ -1727,6 +1990,8 @@ const WIDGET_STYLES = `
1727
1990
  .status-icon img { width: 16px; height: 16px; object-fit: contain; }
1728
1991
  .status-text { color: var(--kp-accent); font-size: 12px; font-weight: 600; flex: 1; }
1729
1992
 
1993
+ /* ── Review body ────────────────────────────────────────────────────────── */
1994
+
1730
1995
  .kp-review-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
1731
1996
  .kp-review-body::-webkit-scrollbar { width: 3px; }
1732
1997
  .kp-review-body::-webkit-scrollbar-thumb { background: rgba(99,102,241,0.2); border-radius: 10px; }
@@ -1758,6 +2023,8 @@ const WIDGET_STYLES = `
1758
2023
 
1759
2024
  .kp-review-crypto-line.loading { opacity: 0.4; }
1760
2025
 
2026
+ /* ── Detail / fee blocks ────────────────────────────────────────────────── */
2027
+
1761
2028
  .kp-detail-block {
1762
2029
  background: var(--kp-surface);
1763
2030
  border: 1px solid var(--kp-border);
@@ -1767,9 +2034,7 @@ const WIDGET_STYLES = `
1767
2034
  }
1768
2035
 
1769
2036
  .kp-detail-row {
1770
- display: flex;
1771
- justify-content: space-between;
1772
- align-items: center;
2037
+ display: flex; justify-content: space-between; align-items: center;
1773
2038
  padding: 11px 16px;
1774
2039
  border-bottom: 1px solid var(--kp-border);
1775
2040
  }
@@ -1793,7 +2058,7 @@ const WIDGET_STYLES = `
1793
2058
  }
1794
2059
 
1795
2060
  .kp-detail-val.accent { color: var(--kp-accent); }
1796
- .kp-detail-val.green { color: var(--kp-green); }
2061
+ .kp-detail-val.green { color: var(--kp-green); }
1797
2062
 
1798
2063
  .kp-fee-block {
1799
2064
  background: rgba(16,185,129,0.04);
@@ -1823,6 +2088,8 @@ const WIDGET_STYLES = `
1823
2088
 
1824
2089
  .kp-fee-header .material-symbols-outlined { font-size: 13px; color: var(--kp-green); }
1825
2090
 
2091
+ /* ── Transaction receipt ────────────────────────────────────────────────── */
2092
+
1826
2093
  .tx-details {
1827
2094
  width: 100%;
1828
2095
  background: var(--kp-surface);
@@ -1833,17 +2100,15 @@ const WIDGET_STYLES = `
1833
2100
  }
1834
2101
 
1835
2102
  .tx-row {
1836
- display: flex;
1837
- justify-content: space-between;
1838
- align-items: center;
2103
+ display: flex; justify-content: space-between; align-items: center;
1839
2104
  padding: 10px 16px;
1840
2105
  border-bottom: 1px solid var(--kp-border);
1841
2106
  }
1842
2107
 
1843
2108
  .tx-row:last-child { border-bottom: none; }
1844
2109
 
1845
- .tx-label { font-family: var(--kp-mono); color: var(--kp-muted); font-size: 10px; letter-spacing: 0.02em; font-weight: 400; }
1846
- .tx-value { color: var(--kp-text); font-size: 11px; font-weight: 500; font-family: var(--kp-mono); }
2110
+ .tx-label { font-family: var(--kp-mono); color: var(--kp-muted); font-size: 10px; letter-spacing: 0.02em; font-weight: 400; }
2111
+ .tx-value { color: var(--kp-text); font-size: 11px; font-weight: 500; font-family: var(--kp-mono); }
1847
2112
  .tx-hash-row { display: flex; align-items: center; gap: 8px; }
1848
2113
 
1849
2114
  .explorer-link {
@@ -1860,26 +2125,57 @@ const WIDGET_STYLES = `
1860
2125
  .explorer-link:hover { background: rgba(99,102,241,0.2); }
1861
2126
  .explorer-link .material-symbols-outlined { font-size: 12px; color: var(--kp-accent); }
1862
2127
 
2128
+
2129
+
2130
+ /* Full-width track that drains left-to-right */
2131
+ .kp-countdown-track {
2132
+ width: 100%;
2133
+ height: 3px;
2134
+ border-radius: 999px;
2135
+ background: var(--kp-surface-2);
2136
+ overflow: hidden;
2137
+ margin-bottom: 6px;
2138
+ }
2139
+
2140
+ .kp-countdown-bar {
2141
+ height: 100%;
2142
+ width: 100%;
2143
+ border-radius: 999px;
2144
+ background: var(--kp-accent);
2145
+ /* smooth drain: transition only width changes */
2146
+ transition: width 0.9s linear, background 0.4s ease;
2147
+ }
2148
+
2149
+ .kp-countdown-label {
2150
+ font-family: var(--kp-mono);
2151
+ font-size: 10px;
2152
+ color: var(--kp-muted);
2153
+ text-align: center;
2154
+ letter-spacing: 0.04em;
2155
+ font-weight: 400;
2156
+ margin-bottom: 2px;
2157
+ }
2158
+
2159
+
2160
+
1863
2161
  .mobile-instruction {
1864
2162
  background: var(--kp-accent-dim);
1865
2163
  border: 1px solid rgba(99,102,241,0.2);
1866
2164
  border-radius: 10px;
1867
2165
  padding: 12px;
1868
2166
  margin: 12px 0 0;
1869
- display: flex;
1870
- align-items: flex-start;
1871
- gap: 10px;
2167
+ display: flex; align-items: flex-start; gap: 10px;
1872
2168
  }
1873
2169
 
1874
2170
  .mobile-instruction-icon { color: var(--kp-accent); font-size: 18px; flex-shrink: 0; margin-top: 1px; }
1875
2171
  .mobile-instruction-text { flex: 1; }
1876
2172
  .mobile-instruction-title { color: var(--kp-text); font-size: 12px; font-weight: 600; margin-bottom: 3px; }
1877
- .mobile-instruction-desc { color: var(--kp-muted); font-size: 11px; line-height: 1.5; font-weight: 400; }
2173
+ .mobile-instruction-desc { color: var(--kp-muted); font-size: 11px; line-height: 1.5; font-weight: 400; }
2174
+
2175
+ /* ── Quote timer ────────────────────────────────────────────────────────── */
1878
2176
 
1879
2177
  .kp-quote-timer {
1880
- display: flex;
1881
- align-items: center;
1882
- gap: 5px;
2178
+ display: flex; align-items: center; gap: 5px;
1883
2179
  font-family: var(--kp-mono);
1884
2180
  font-size: 10px;
1885
2181
  color: var(--kp-muted);
@@ -1887,9 +2183,33 @@ const WIDGET_STYLES = `
1887
2183
  font-weight: 400;
1888
2184
  }
1889
2185
 
1890
- .kp-quote-timer .material-symbols-outlined { font-size: 12px; }
1891
- .kp-quote-timer.urgent { color: #fb923c; }
1892
- .kp-quote-timer.expired { color: var(--kp-red); }
2186
+ .kp-quote-timer .material-symbols-outlined { font-size: 12px; }
2187
+ .kp-quote-timer.urgent { color: #fb923c; }
2188
+ .kp-quote-timer.expired { color: var(--kp-red); }
2189
+
2190
+ /* ── WalletConnect mobile ───────────────────────────────────────────────── */
2191
+
2192
+ .kp-mobile-connect-status {
2193
+ display: flex; align-items: center; gap: 12px;
2194
+ background: var(--kp-accent-dim);
2195
+ border: 1px solid rgba(99,102,241,0.2);
2196
+ border-radius: 12px;
2197
+ padding: 14px 16px;
2198
+ margin-bottom: 4px;
2199
+ }
2200
+
2201
+ .kp-mobile-status-icon { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
2202
+ .kp-mobile-status-text { flex: 1; }
2203
+ .kp-mobile-status-title { font-size: 13px; font-weight: 600; color: var(--kp-text); margin-bottom: 2px; }
2204
+ .kp-mobile-status-desc { font-family: var(--kp-mono); font-size: 11px; color: var(--kp-muted); }
2205
+
2206
+ .kp-wallet-option:active {
2207
+ transform: scale(0.98);
2208
+ background: var(--kp-accent-dim);
2209
+ border-color: var(--kp-border-active);
2210
+ }
2211
+
2212
+
1893
2213
 
1894
2214
  @media (max-width: 480px) {
1895
2215
  .kwespay-overlay {
@@ -1928,66 +2248,25 @@ const WIDGET_STYLES = `
1928
2248
  }
1929
2249
  }
1930
2250
 
1931
-
1932
- .kp-mobile-connect-status {
1933
- display: flex;
1934
- align-items: center;
1935
- gap: 12px;
1936
- background: var(--kp-accent-dim);
1937
- border: 1px solid rgba(99,102,241,0.2);
1938
- border-radius: 12px;
1939
- padding: 14px 16px;
1940
- margin-bottom: 4px;
1941
- }
1942
-
1943
- .kp-mobile-status-icon {
1944
- width: 28px; height: 28px;
1945
- display: flex; align-items: center; justify-content: center;
1946
- flex-shrink: 0;
1947
- }
1948
-
1949
- .kp-mobile-status-text { flex: 1; }
1950
-
1951
- .kp-mobile-status-title {
1952
- font-size: 13px;
1953
- font-weight: 600;
1954
- color: var(--kp-text);
1955
- margin-bottom: 2px;
1956
- }
1957
-
1958
- .kp-mobile-status-desc {
1959
- font-family: var(--kp-mono);
1960
- font-size: 11px;
1961
- color: var(--kp-muted);
1962
- }
1963
-
1964
- .kp-wallet-option:active {
1965
- transform: scale(0.98);
1966
- background: var(--kp-accent-dim);
1967
- border-color: var(--kp-border-active);
1968
- }
1969
-
1970
-
1971
2251
  @keyframes kpSheetUp {
1972
2252
  from { transform: translateY(100%); opacity: 0.8; }
1973
- to { transform: translateY(0); opacity: 1; }
2253
+ to { transform: translateY(0); opacity: 1; }
1974
2254
  }
1975
2255
 
1976
2256
  @keyframes kpSheetDown {
1977
- from { transform: translateY(0); opacity: 1; }
2257
+ from { transform: translateY(0); opacity: 1; }
1978
2258
  to { transform: translateY(100%); opacity: 0; }
1979
2259
  }
1980
2260
  `;
1981
2261
 
1982
2262
  function getStepTemplates(fiatAmount, currency) {
1983
2263
  return `
2264
+ <!-- Step 0: Initialising -->
1984
2265
  <div class="step active" id="kwespay-step0">
1985
2266
  <div class="kp-topbar">
1986
2267
  <div class="kp-topbar-brand">
1987
- <div class="kp-topbar-dot"></div>
1988
2268
  <span class="kp-topbar-name">KwesPay Checkout</span>
1989
2269
  </div>
1990
-
1991
2270
  </div>
1992
2271
  <div class="loading-container" style="flex:1">
1993
2272
  <div class="spinner-wrapper">
@@ -2006,10 +2285,10 @@ function getStepTemplates(fiatAmount, currency) {
2006
2285
  </div>
2007
2286
  </div>
2008
2287
 
2288
+ <!-- Step 0.5: API key invalid / network error on init -->
2009
2289
  <div class="step" id="kwespay-step0-invalid">
2010
2290
  <div class="kp-topbar">
2011
2291
  <div class="kp-topbar-brand">
2012
- <div class="kp-topbar-dot" style="background:var(--kp-red);box-shadow:none;animation:none"></div>
2013
2292
  <span class="kp-topbar-name">KwesPay Checkout</span>
2014
2293
  </div>
2015
2294
  </div>
@@ -2025,13 +2304,12 @@ function getStepTemplates(fiatAmount, currency) {
2025
2304
  </div>
2026
2305
  </div>
2027
2306
 
2307
+ <!-- Step 1: Select Network -->
2028
2308
  <div class="step" id="kwespay-step1">
2029
2309
  <div class="kp-topbar">
2030
2310
  <div class="kp-topbar-brand">
2031
- <div class="kp-topbar-dot"></div>
2032
2311
  <span class="kp-topbar-name">KwesPay Checkout</span>
2033
2312
  </div>
2034
-
2035
2313
  </div>
2036
2314
  <div class="kp-amount-block">
2037
2315
  <div class="kp-amount-label">Total due</div>
@@ -2059,21 +2337,19 @@ function getStepTemplates(fiatAmount, currency) {
2059
2337
  </div>
2060
2338
  </div>
2061
2339
 
2340
+ <!-- Step 2: Select Token -->
2062
2341
  <div class="step" id="kwespay-step2">
2063
2342
  <div class="kp-topbar">
2064
2343
  <div class="kp-topbar-brand">
2065
2344
  <button class="kp-back-btn" id="kwespay-back2">
2066
2345
  <span class="material-symbols-outlined">arrow_back</span>
2067
2346
  </button>
2068
- <div class="kp-topbar-dot"></div>
2069
2347
  <span class="kp-topbar-name">KwesPay Checkout</span>
2070
2348
  </div>
2071
-
2072
2349
  </div>
2073
2350
  <div class="kp-amount-block">
2074
2351
  <div class="kp-amount-label">Total due</div>
2075
2352
  <div class="kp-amount-value">${fiatAmount} ${currency}</div>
2076
- <div class="kp-amount-hint">Select a token — exact amount shown at review</div>
2077
2353
  </div>
2078
2354
  <div class="progress-section">
2079
2355
  <div class="progress-info">
@@ -2102,16 +2378,15 @@ function getStepTemplates(fiatAmount, currency) {
2102
2378
  </div>
2103
2379
  </div>
2104
2380
 
2381
+ <!-- Step 3: Review & Pay -->
2105
2382
  <div class="step" id="kwespay-step3">
2106
2383
  <div class="kp-topbar">
2107
2384
  <div class="kp-topbar-brand">
2108
2385
  <button class="kp-back-btn" id="kwespay-back3">
2109
2386
  <span class="material-symbols-outlined">arrow_back</span>
2110
2387
  </button>
2111
- <div class="kp-topbar-dot"></div>
2112
2388
  <span class="kp-topbar-name">KwesPay Checkout</span>
2113
2389
  </div>
2114
-
2115
2390
  </div>
2116
2391
  <div class="progress-section">
2117
2392
  <div class="progress-info">
@@ -2133,37 +2408,35 @@ function getStepTemplates(fiatAmount, currency) {
2133
2408
  <span id="kwespay-quoteTimerText">—</span>
2134
2409
  </div>
2135
2410
  </div>
2136
-
2137
2411
  <div class="kp-detail-block">
2138
2412
  <div class="kp-detail-row">
2139
- <span class="kp-detail-key">wallet</span>
2413
+ <span class="kp-detail-key">Wallet</span>
2140
2414
  <span class="kp-detail-val" id="kwespay-connectedWalletAddress">—</span>
2141
2415
  </div>
2142
2416
  <div class="kp-detail-row">
2143
- <span class="kp-detail-key">network</span>
2417
+ <span class="kp-detail-key">Network</span>
2144
2418
  <span class="kp-detail-val" id="kwespay-summaryNetwork">—</span>
2145
2419
  </div>
2146
2420
  <div class="kp-detail-row">
2147
- <span class="kp-detail-key">token</span>
2421
+ <span class="kp-detail-key">Token</span>
2148
2422
  <span class="kp-detail-val accent" id="kwespay-summaryToken">—</span>
2149
2423
  </div>
2150
2424
  </div>
2151
-
2152
2425
  <div class="kp-fee-block">
2153
2426
  <div class="kp-fee-header">
2154
2427
  <span class="material-symbols-outlined">receipt_long</span>
2155
2428
  <span class="kp-fee-header-text">Fee Breakdown</span>
2156
2429
  </div>
2157
2430
  <div class="kp-detail-row">
2158
- <span class="kp-detail-key">payment amount</span>
2431
+ <span class="kp-detail-key">Payment amount</span>
2159
2432
  <span class="kp-detail-val" id="kwespay-feePaymentAmount">—</span>
2160
2433
  </div>
2161
2434
  <div class="kp-detail-row">
2162
- <span class="kp-detail-key">platform fee (0.25%)</span>
2435
+ <span class="kp-detail-key">Platform fee (0.25%)</span>
2163
2436
  <span class="kp-detail-val" id="kwespay-feePlatformFee">—</span>
2164
2437
  </div>
2165
2438
  <div class="kp-detail-row">
2166
- <span class="kp-detail-key">vendor receives</span>
2439
+ <span class="kp-detail-key">Vendor receives</span>
2167
2440
  <span class="kp-detail-val green" id="kwespay-feeVendorAmount">—</span>
2168
2441
  </div>
2169
2442
  </div>
@@ -2174,14 +2447,18 @@ function getStepTemplates(fiatAmount, currency) {
2174
2447
  </div>
2175
2448
  </div>
2176
2449
 
2450
+ <!-- Step 4: Processing / Confirming / Success (mutating sub-views) -->
2177
2451
  <div class="step" id="kwespay-step4">
2178
2452
  <div class="kp-topbar">
2179
2453
  <div class="kp-topbar-brand">
2180
- <div class="kp-topbar-dot"></div>
2181
- <span class="kp-topbar-name">KwesPay Checkout</span>
2454
+ <div class="kp-topbar-dot" id="kwespay-step4-dot"></div>
2455
+ <span class="kp-topbar-name" id="kwespay-step4-title">KwesPay Checkout</span>
2182
2456
  </div>
2457
+
2183
2458
  </div>
2184
- <div class="loading-container" style="flex:1">
2459
+
2460
+ <!-- Sub-view: processing (wallet approval + on-chain) -->
2461
+ <div id="kwespay-view-processing" class="loading-container" style="flex:1">
2185
2462
  <div class="spinner-wrapper">
2186
2463
  <div class="spinner-ring"></div>
2187
2464
  <div class="spinner-ring-2"></div>
@@ -2199,62 +2476,92 @@ function getStepTemplates(fiatAmount, currency) {
2199
2476
  </div>
2200
2477
  </div>
2201
2478
  </div>
2202
- </div>
2203
2479
 
2204
- <div class="step" id="kwespay-step5">
2205
- <div class="kp-topbar">
2206
- <div class="kp-topbar-brand">
2207
- <div class="kp-topbar-dot" style="background:var(--kp-green);box-shadow:0 0 8px rgba(16,185,129,0.4);animation:none"></div>
2208
- <span class="kp-topbar-name">Payment Complete</span>
2209
- </div>
2210
- <div class="kp-topbar-secure">
2211
- <span class="material-symbols-outlined" style="color:var(--kp-green)">check_circle</span>
2212
- CONFIRMED
2480
+ <!-- Sub-view: confirming (on-chain done, polling backend) -->
2481
+ <div id="kwespay-view-confirming" class="loading-container" style="flex:1;display:none">
2482
+ <div class="spinner-wrapper">
2483
+ <div class="spinner-ring kp-spin-soft"></div>
2484
+ <div class="spinner-ring-2 kp-spin-soft"></div>
2485
+ <div class="spinner-icon kp-spinner-icon-green">
2486
+ <span class="material-symbols-outlined" style="color:var(--kp-green)">cloud_sync</span>
2487
+ </div>
2213
2488
  </div>
2489
+ <h2 class="headline">Confirming Payment</h2>
2490
+ <p class="body-text" id="kwespay-confirmingText">Waiting for network confirmation.</p>
2214
2491
  </div>
2215
- <div class="loading-container" style="flex:1">
2216
- <div class="success-icon">
2217
- <span class="material-symbols-outlined">check_circle</span>
2492
+
2493
+ <!-- Sub-view: success receipt -->
2494
+ <div id="kwespay-view-success" style="display:none;flex-direction:column;flex:1;overflow:hidden">
2495
+
2496
+ <!-- Thin green confirmed bar -->
2497
+ <div class="kp-confirmed-bar">
2498
+ <span class="material-symbols-outlined" style="font-size:13px;color:var(--kp-green)">check_circle</span>
2499
+ <span>Payment confirmed on-chain</span>
2218
2500
  </div>
2219
- <h2 class="headline">Payment Successful</h2>
2220
- <p class="body-text">Your transaction has been confirmed on-chain.</p>
2221
- <div class="tx-details">
2222
- <div class="tx-row">
2223
- <span class="tx-label">tx hash</span>
2224
- <div class="tx-hash-row">
2225
- <span class="tx-value" id="kwespay-txHash">—</span>
2226
- <a class="explorer-link" id="kwespay-explorerLink" target="_blank" rel="noopener noreferrer">
2227
- <span class="material-symbols-outlined">open_in_new</span>
2228
- </a>
2229
- </div>
2230
- </div>
2231
- <div class="tx-row">
2232
- <span class="tx-label">amount paid</span>
2233
- <span class="tx-value" id="kwespay-txFiatAmount">${fiatAmount} ${currency}</span>
2501
+
2502
+ <!-- Scrollable receipt body -->
2503
+ <div class="loading-container" style="flex:1;padding-bottom:0;justify-content:flex-start;padding-top:20px;overflow-y:auto">
2504
+ <div class="success-icon">
2505
+ <span class="material-symbols-outlined">check_circle</span>
2234
2506
  </div>
2235
- <div class="tx-row">
2236
- <span class="tx-label">crypto amount</span>
2237
- <span class="tx-value" id="kwespay-txCryptoAmount">—</span>
2507
+ <h2 class="headline">Payment Successful</h2>
2508
+ <p class="body-text">Your transaction has been confirmed on-chain.</p>
2509
+
2510
+ <div class="tx-details">
2511
+ <div class="tx-row">
2512
+ <span class="tx-label">Tx hash</span>
2513
+ <div class="tx-hash-row">
2514
+ <span class="tx-value" id="kwespay-txHash">—</span>
2515
+ <a class="explorer-link" id="kwespay-explorerLink" target="_blank" rel="noopener noreferrer">
2516
+ <span class="material-symbols-outlined">open_in_new</span>
2517
+ </a>
2518
+ </div>
2519
+ </div>
2520
+ <div class="tx-row">
2521
+ <span class="tx-label">Reference</span>
2522
+ <span class="tx-value" id="kwespay-txRef">—</span>
2523
+ </div>
2524
+ <div class="tx-row">
2525
+ <span class="tx-label">Amount paid</span>
2526
+ <span class="tx-value" id="kwespay-txFiatAmount">${fiatAmount} ${currency}</span>
2527
+ </div>
2528
+ <div class="tx-row">
2529
+ <span class="tx-label">Crypto amount</span>
2530
+ <span class="tx-value" id="kwespay-txCryptoAmount">—</span>
2531
+ </div>
2532
+ <div class="tx-row">
2533
+ <span class="tx-label">Network</span>
2534
+ <span class="tx-value" id="kwespay-txNetwork">—</span>
2535
+ </div>
2536
+ <div class="tx-row">
2537
+ <span class="tx-label">Time</span>
2538
+ <span class="tx-value" id="kwespay-txTime">—</span>
2539
+ </div>
2238
2540
  </div>
2239
- <div class="tx-row">
2240
- <span class="tx-label">network</span>
2241
- <span class="tx-value" id="kwespay-txNetwork">—</span>
2541
+ </div>
2542
+
2543
+ <!-- Countdown + Done button -->
2544
+ <div class="bottom-action" style="padding-top:10px">
2545
+ <!-- Countdown bar -->
2546
+ <div class="kp-countdown-track">
2547
+ <div class="kp-countdown-bar" id="kwespay-receiptCountdownBar"></div>
2242
2548
  </div>
2549
+ <p class="kp-countdown-label" id="kwespay-receiptCountdown">Closing in 10s</p>
2550
+ <button class="action-btn" id="kwespay-closeSuccessBtn" style="margin-top:8px">Done</button>
2551
+ </div>
2552
+
2553
+ <div class="kp-footer">
2554
+ <span class="material-symbols-outlined kp-footer-lock" style="font-size:12px">lock</span>
2555
+ <span class="kp-footer-text">Secured by <span>KwesPay</span> &middot; On-chain verified</span>
2243
2556
  </div>
2244
- </div>
2245
- <div class="bottom-action">
2246
- <button class="action-btn" id="kwespay-closeSuccessBtn">Done</button>
2247
- </div>
2248
- <div class="kp-footer">
2249
- <span class="material-symbols-outlined kp-footer-lock" style="font-size:12px">lock</span>
2250
- <span class="kp-footer-text">Secured by <span>KwesPay</span> &middot; On-chain verified</span>
2251
2557
  </div>
2252
2558
  </div>
2253
2559
 
2254
- <div class="step" id="kwespay-step6">
2560
+ <!-- Step 5: Error / Failed -->
2561
+ <div class="step" id="kwespay-step5">
2255
2562
  <div class="kp-topbar">
2256
2563
  <div class="kp-topbar-brand">
2257
- <div class="kp-topbar-dot" style="background:var(--kp-red);box-shadow:none;animation:none"></div>
2564
+ <div class="kp-topbar-dot" style="background:var(--kp-red);box-shadow:0 0 8px rgba(244,63,94,0.35);animation:none"></div>
2258
2565
  <span class="kp-topbar-name">Payment Failed</span>
2259
2566
  </div>
2260
2567
  </div>
@@ -2264,6 +2571,12 @@ function getStepTemplates(fiatAmount, currency) {
2264
2571
  </div>
2265
2572
  <h2 class="headline" id="kwespay-errorTitle">Something went wrong</h2>
2266
2573
  <p class="body-text" id="kwespay-errorMessage">An error occurred while processing your payment.</p>
2574
+
2575
+ <!-- Hint strip — gives context without overwhelming the user -->
2576
+ <div class="kp-error-hint">
2577
+ <span class="material-symbols-outlined" style="font-size:14px;color:var(--kp-muted)">info</span>
2578
+ <span>Your funds have not been charged. You can retry safely.</span>
2579
+ </div>
2267
2580
  </div>
2268
2581
  <div class="bottom-action">
2269
2582
  <button class="action-btn" id="kwespay-retryPayment">Try Again</button>
@@ -2310,27 +2623,29 @@ const DomMethods = {
2310
2623
  overlay.className = "kwespay-overlay";
2311
2624
  overlay.id = "kwespay-widget-overlay";
2312
2625
 
2626
+ const container = document.createElement("div");
2627
+ container.className = "kwespay-container";
2628
+ container.id = "kwespay-widget-container";
2629
+
2313
2630
  const closeBtn = document.createElement("button");
2314
2631
  closeBtn.className = "kwespay-close-btn";
2315
2632
  closeBtn.innerHTML = "×";
2316
2633
  closeBtn.onclick = () => this.close();
2317
- overlay.appendChild(closeBtn);
2634
+ container.appendChild(closeBtn);
2318
2635
 
2319
- const container = document.createElement("div");
2320
- container.className = "kwespay-container";
2321
- container.id = "kwespay-widget-container";
2322
- container.innerHTML = getStepTemplates(
2323
- this.config.amount,
2636
+ const stepsWrapper = document.createElement("div");
2637
+ stepsWrapper.className = "kwespay-steps-wrapper";
2638
+ stepsWrapper.innerHTML = getStepTemplates(
2639
+ this._displayAmount,
2324
2640
  this.config.currency
2325
2641
  );
2642
+ container.appendChild(stepsWrapper);
2326
2643
 
2327
2644
  overlay.appendChild(container);
2328
2645
  document.body.appendChild(overlay);
2329
2646
 
2330
2647
  this._setupEventListeners();
2331
2648
  this._setupSwipeToClose(container);
2332
-
2333
- // Clicking outside the container does NOT close the widget (intentional)
2334
2649
  },
2335
2650
 
2336
2651
  _setupSwipeToClose(container) {
@@ -2501,8 +2816,11 @@ const NavMethods = {
2501
2816
  targetStep = document.getElementById(`kwespay-step-${stepNumber}`);
2502
2817
  else targetStep = document.getElementById(`kwespay-step${stepNumber}`);
2503
2818
 
2504
- if (targetStep) targetStep.classList.add("active");
2505
- else console.warn(`[KwesPayWidget] Step not found: ${stepNumber}`);
2819
+ if (targetStep) {
2820
+ targetStep.classList.add("active");
2821
+ } else {
2822
+ console.warn(`[KwesPayWidget] Step not found: ${stepNumber}`);
2823
+ }
2506
2824
  },
2507
2825
 
2508
2826
  _removeCustomStep(id) {
@@ -2512,13 +2830,16 @@ const NavMethods = {
2512
2830
  _showError(title, message) {
2513
2831
  const titleEl = document.getElementById("kwespay-errorTitle");
2514
2832
  const msgEl = document.getElementById("kwespay-errorMessage");
2515
- if (titleEl) titleEl.textContent = title;
2516
- if (msgEl) msgEl.textContent = message;
2517
- this._goToStep(6);
2833
+ if (titleEl) titleEl.textContent = title ?? "Something went wrong";
2834
+ if (msgEl)
2835
+ msgEl.textContent =
2836
+ message ?? "An unexpected error occurred. Please try again.";
2837
+ this._goToStep(5);
2518
2838
  },
2519
2839
 
2520
2840
  _reset() {
2521
2841
  this._clearQuoteTimer();
2842
+ this._stopReceiptCountdown();
2522
2843
  this._removeCustomStep("kwespay-step-wallet-picker");
2523
2844
  this._removeCustomStep("kwespay-step-wc");
2524
2845
  this.state.selectedNetwork = null;
@@ -2586,23 +2907,13 @@ const NetworkMethods = {
2586
2907
  const listItem = document.createElement("div");
2587
2908
  listItem.className = "list-item";
2588
2909
  listItem.innerHTML = `
2589
- <div class="item-icon"><img src="${network.logo}" alt="${
2590
- network.name
2591
- }" /></div>
2910
+ <div class="item-icon"><img src="${network.logo}" alt="${network.name}" /></div>
2592
2911
  <div class="item-info">
2593
2912
  <div class="item-name-row">
2594
2913
  <p class="item-name">${network.name}</p>
2595
- ${
2596
- network.type === "testnet"
2597
- ? '<span class="item-badge badge-testnet">Testnet</span>'
2598
- : ""
2599
- }
2914
+
2600
2915
  </div>
2601
- <p class="item-desc">${
2602
- network.type === "mainnet"
2603
- ? "Mainnet · Production"
2604
- : "Testnet · Development"
2605
- }</p>
2916
+
2606
2917
  </div>
2607
2918
  <span class="material-symbols-outlined item-chevron">chevron_right</span>
2608
2919
  `;
@@ -2741,7 +3052,7 @@ const WalletMethods = {
2741
3052
  <button class="kp-back-btn" id="kwespay-picker-back">
2742
3053
  <span class="material-symbols-outlined">arrow_back</span>
2743
3054
  </button>
2744
- <div class="kp-topbar-dot"></div>
3055
+
2745
3056
  <span class="kp-topbar-name">Connect Wallet</span>
2746
3057
  </div>
2747
3058
  </div>
@@ -2911,7 +3222,7 @@ const WalletMethods = {
2911
3222
  <button class="kp-back-btn" id="kwespay-wc-back">
2912
3223
  <span class="material-symbols-outlined">arrow_back</span>
2913
3224
  </button>
2914
- <div class="kp-topbar-dot"></div>
3225
+
2915
3226
  <span class="kp-topbar-name">Scan QR Code</span>
2916
3227
  </div>
2917
3228
  </div>
@@ -2951,7 +3262,7 @@ const WalletMethods = {
2951
3262
  <button class="kp-back-btn" id="kwespay-wc-back">
2952
3263
  <span class="material-symbols-outlined">arrow_back</span>
2953
3264
  </button>
2954
- <div class="kp-topbar-dot"></div>
3265
+
2955
3266
  <span class="kp-topbar-name">Connect Wallet</span>
2956
3267
  </div>
2957
3268
  </div>
@@ -3101,8 +3412,6 @@ const WalletMethods = {
3101
3412
  },
3102
3413
  };
3103
3414
 
3104
- const PLATFORM_FEE_BPS = 25;
3105
-
3106
3415
  function formatUnits(rawBigInt, decimals) {
3107
3416
  const divisor = BigInt(10 ** decimals);
3108
3417
  const whole = rawBigInt / divisor;
@@ -3130,6 +3439,31 @@ function formatUnits(rawBigInt, decimals) {
3130
3439
  return `0.${fracFull.slice(0, firstSig) + sigSlice}`;
3131
3440
  }
3132
3441
 
3442
+ function _validateQuoteDecimals(
3443
+ amountBaseUnits,
3444
+ fiatAmount,
3445
+ decimals,
3446
+ usdPrice
3447
+ ) {
3448
+ try {
3449
+ if (!usdPrice || usdPrice <= 0) return { ok: true };
3450
+ const humanAmount = Number(BigInt(amountBaseUnits)) / 10 ** decimals;
3451
+ const impliedUsd = humanAmount * usdPrice;
3452
+ const ratio = impliedUsd / fiatAmount;
3453
+ if (ratio < 0.5 || ratio > 10) {
3454
+ return {
3455
+ ok: false,
3456
+ reason: `Amount sanity check failed: got ${humanAmount} (≈$${impliedUsd.toFixed(
3457
+ 6
3458
+ )}) for $${fiatAmount} fiat.`,
3459
+ };
3460
+ }
3461
+ return { ok: true };
3462
+ } catch {
3463
+ return { ok: true };
3464
+ }
3465
+ }
3466
+
3133
3467
  const QuoteMethods = {
3134
3468
  async _loadReviewStep() {
3135
3469
  this._clearQuoteTimer();
@@ -3142,45 +3476,63 @@ const QuoteMethods = {
3142
3476
  const proceedBtn = document.getElementById("kwespay-proceedToPayment");
3143
3477
 
3144
3478
  if (cryptoLine) {
3145
- cryptoLine.textContent = "loading";
3479
+ cryptoLine.textContent = "loading...";
3146
3480
  cryptoLine.classList.add("loading");
3147
3481
  }
3148
3482
  if (timerEl) timerEl.style.display = "none";
3149
3483
  if (proceedBtn) proceedBtn.disabled = true;
3150
3484
 
3151
3485
  try {
3152
- const payload = await this.paymentService.getQuote({
3153
- vendorId: this.config.vendorId,
3486
+ const quote = await this.paymentService.getQuote({
3487
+ vendorIdentifier: this.config.vendorId,
3154
3488
  cryptoCurrency: this.state.selectedToken,
3155
3489
  fiatAmount: this.config.amount,
3156
3490
  fiatCurrency: this.config.currency,
3157
3491
  network: this.state.selectedNetwork,
3158
- payerWalletAddress: this.walletService.getAddress(),
3159
3492
  });
3160
3493
 
3161
- this.state.currentPayload = payload;
3162
-
3163
- const decimals = this.state.selectedTokenConfig?.decimals ?? 6;
3164
- const amountBig = BigInt(payload.amountBaseUnits);
3165
- const feeNum = (Number(amountBig) * PLATFORM_FEE_BPS) / 10000;
3166
- const feeBig = BigInt(Math.max(1, Math.round(feeNum)));
3167
- const vendorBig = amountBig - feeBig;
3494
+ const decimals = this.state.selectedTokenConfig?.decimals ?? 18;
3168
3495
  const sym = this.state.selectedToken;
3169
- const fmt = (n) => `${formatUnits(n, decimals)} ${sym}`;
3496
+
3497
+ const check = _validateQuoteDecimals(
3498
+ quote.amountBaseUnits,
3499
+ this.config.amount,
3500
+ decimals,
3501
+ quote.usdPriceSnapshot
3502
+ );
3503
+ if (!check.ok) {
3504
+ console.error("[KwesPay] Quote sanity check failed:", check.reason);
3505
+ throw new Error(
3506
+ `Quote returned an implausible amount for ${sym}. Please contact support.`
3507
+ );
3508
+ }
3509
+
3510
+ this.state.currentPayload = {
3511
+ ...quote,
3512
+ vendorIdentifier: this.config.vendorId,
3513
+ fiatAmount: this.config.amount,
3514
+ fiatCurrency: this.config.currency,
3515
+ };
3516
+
3517
+ const fmt = (n) =>
3518
+ formatCryptoAmount(parseFloat(formatUnits(n, decimals)), sym);
3519
+
3520
+ const amountBig = BigInt(quote.amountBaseUnits);
3521
+ const totalBig = BigInt(quote.totalBaseUnits);
3522
+ const feeBig = totalBig - amountBig;
3170
3523
 
3171
3524
  if (cryptoLine) {
3172
- cryptoLine.textContent = fmt(amountBig);
3525
+ cryptoLine.textContent = fmt(totalBig);
3173
3526
  cryptoLine.classList.remove("loading");
3174
3527
  }
3175
- if (feeAmount) feeAmount.textContent = fmt(amountBig);
3528
+ if (feeAmount) feeAmount.textContent = fmt(totalBig);
3176
3529
  if (feePlatform) feePlatform.textContent = fmt(feeBig);
3177
- if (feeVendor)
3178
- feeVendor.textContent = fmt(vendorBig < 0n ? 0n : vendorBig);
3530
+ if (feeVendor) feeVendor.textContent = fmt(amountBig);
3179
3531
  if (proceedBtn) proceedBtn.disabled = false;
3180
3532
 
3181
- this._startQuoteTimer(payload.expiresAt);
3533
+ this._startQuoteTimer(quote.expiresAt);
3182
3534
  } catch (err) {
3183
- console.error("[KwesPayWidget] Quote fetch failed:", err.message);
3535
+ console.error("[KwesPay] Quote fetch failed:", err.message);
3184
3536
  if (cryptoLine) {
3185
3537
  cryptoLine.textContent = "Could not load please try again.";
3186
3538
  cryptoLine.classList.remove("loading");
@@ -3200,7 +3552,8 @@ const QuoteMethods = {
3200
3552
  const secs = Math.ceil(remaining / 1000);
3201
3553
  const mins = Math.floor(secs / 60);
3202
3554
  const s = secs % 60;
3203
- timerText.textContent = `Price locks in ${mins}:${String(s).padStart(
3555
+
3556
+ timerText.textContent = `Quote expires in ${mins}:${String(s).padStart(
3204
3557
  2,
3205
3558
  "0"
3206
3559
  )}`;
@@ -3210,7 +3563,7 @@ const QuoteMethods = {
3210
3563
  (secs <= 0 ? " expired" : "");
3211
3564
 
3212
3565
  if (secs <= 0) {
3213
- timerText.textContent = "Refreshing your rate";
3566
+ timerText.textContent = "Refreshing your rate...";
3214
3567
  const proceedBtn = document.getElementById("kwespay-proceedToPayment");
3215
3568
  if (proceedBtn) proceedBtn.disabled = true;
3216
3569
  this._clearQuoteTimer();
@@ -3230,6 +3583,8 @@ const QuoteMethods = {
3230
3583
  },
3231
3584
  };
3232
3585
 
3586
+ const RECEIPT_DWELL_MS = 10_000;
3587
+
3233
3588
  const PaymentMethods = {
3234
3589
  async _handlePaymentProcessing() {
3235
3590
  if (!this.state.currentPayload) {
@@ -3243,6 +3598,7 @@ const PaymentMethods = {
3243
3598
  try {
3244
3599
  this._clearQuoteTimer();
3245
3600
  this._goToStep(4);
3601
+ this._setProcessingView("processing");
3246
3602
 
3247
3603
  const setStatus = (title, text) => {
3248
3604
  const titleEl = document.getElementById("kwespay-processingTitle");
@@ -3253,27 +3609,34 @@ const PaymentMethods = {
3253
3609
 
3254
3610
  const isWC = this.walletService.connectionType === "walletconnect";
3255
3611
  const isMobile = this.walletService.isMobile();
3256
- const strictMobile = isWC && isMobile; // mobile WC path — no network switching, no RPC shortcuts
3612
+ const strictMobile = isWC && isMobile;
3257
3613
  const provider = this.walletService.getProvider();
3258
3614
  const targetChainId = this.state.selectedChainId;
3259
3615
 
3260
- if (!provider) throw new Error("No wallet provider");
3261
-
3616
+ if (!provider) {
3617
+ throw Object.assign(
3618
+ new Error(
3619
+ "No wallet provider available. Please reconnect your wallet."
3620
+ ),
3621
+ {
3622
+ code: "NO_PROVIDER",
3623
+ }
3624
+ );
3625
+ }
3262
3626
 
3263
- const alive = await this.walletService.isSessionAlive();
3627
+ const alive = await this.walletService
3628
+ .isSessionAlive()
3629
+ .catch(() => false);
3264
3630
  if (!alive) {
3265
- console.error(
3266
- "[KwesPay] Session liveness check failed — session appears stale"
3267
- );
3268
- await this.walletService.disconnect();
3269
- const err = new Error(
3270
- "Your wallet session expired. Please reconnect your wallet."
3631
+ await this.walletService.disconnect().catch(() => {});
3632
+ throw Object.assign(
3633
+ new Error(
3634
+ "Your wallet session expired. Please reconnect your wallet."
3635
+ ),
3636
+ { code: "SESSION_EXPIRED" }
3271
3637
  );
3272
- err.code = "SESSION_EXPIRED";
3273
- throw err;
3274
3638
  }
3275
3639
 
3276
-
3277
3640
  if (isMobile) {
3278
3641
  document
3279
3642
  .getElementById("kwespay-mobileTransactionInstruction")
@@ -3281,17 +3644,20 @@ const PaymentMethods = {
3281
3644
  }
3282
3645
 
3283
3646
  if (strictMobile) {
3284
-
3285
3647
  await this._assertMobileChain(provider, targetChainId);
3286
3648
  } else {
3287
- const rawChain = await provider.request({ method: "eth_chainId" });
3649
+ let rawChain;
3650
+ try {
3651
+ rawChain = await provider.request({ method: "eth_chainId" });
3652
+ } catch {
3653
+ throw Object.assign(
3654
+ new Error(
3655
+ "Could not read the current network from your wallet. Please try again."
3656
+ ),
3657
+ { code: "NETWORK_ERROR" }
3658
+ );
3659
+ }
3288
3660
  const currentChainId = parseInt(rawChain, 16);
3289
-
3290
- console.log("[KwesPay] Desktop chain check —", {
3291
- currentChainId,
3292
- targetChainId,
3293
- });
3294
-
3295
3661
  if (currentChainId !== targetChainId) {
3296
3662
  setStatus(
3297
3663
  "Switching network…",
@@ -3304,11 +3670,9 @@ const PaymentMethods = {
3304
3670
  this.state.selectedToken,
3305
3671
  this.state.selectedTokenConfig.decimals
3306
3672
  );
3307
- console.log("[KwesPay] Network switched");
3308
3673
  }
3309
3674
  }
3310
3675
 
3311
-
3312
3676
  if (strictMobile) {
3313
3677
  setStatus(
3314
3678
  "Opening your wallet…",
@@ -3323,56 +3687,111 @@ const PaymentMethods = {
3323
3687
  );
3324
3688
  }
3325
3689
 
3326
- const receipt = await this.paymentService.createPayment({
3327
- payload: this.state.currentPayload,
3328
- walletProvider: provider,
3329
- onStatusUpdate: setStatus,
3330
- });
3690
+ let receipt;
3691
+ try {
3692
+ receipt = await this.paymentService.createPayment({
3693
+ payload: this.state.currentPayload,
3694
+ walletProvider: provider,
3695
+ onStatusUpdate: setStatus,
3696
+ });
3697
+ } catch (payErr) {
3698
+ const msg =
3699
+ payErr?.message ?? "Payment submission failed. Please try again.";
3700
+ throw Object.assign(new Error(msg), {
3701
+ code: payErr?.code ?? "CONTRACT_ERROR",
3702
+ original: payErr,
3703
+ });
3704
+ }
3705
+
3706
+ if (!receipt?.hash) {
3707
+ throw Object.assign(
3708
+ new Error(
3709
+ "Transaction was submitted but no hash was returned. Check your wallet for status."
3710
+ ),
3711
+ { code: "MISSING_HASH" }
3712
+ );
3713
+ }
3331
3714
 
3332
-
3333
3715
  document
3334
3716
  .getElementById("kwespay-mobileTransactionInstruction")
3335
3717
  ?.style.setProperty("display", "none");
3336
3718
 
3337
-
3338
- const decimals = this.state.selectedTokenConfig?.decimals ?? 6;
3339
- const amountBig = BigInt(this.state.currentPayload.amountBaseUnits);
3340
- const cryptoDisplay = `${formatUnits(amountBig, decimals)} ${
3341
- this.state.selectedToken
3342
- }`;
3719
+ this._setProcessingView("confirming");
3343
3720
 
3344
- document.getElementById("kwespay-txHash").textContent = truncateHash(
3345
- receipt.hash
3346
- );
3347
- document.getElementById(
3348
- "kwespay-txFiatAmount"
3349
- ).textContent = `${this.config.amount} ${this.config.currency}`;
3350
- document.getElementById("kwespay-txCryptoAmount").textContent =
3351
- cryptoDisplay;
3352
- document.getElementById("kwespay-txNetwork").textContent =
3353
- this.state.selectedNetworkName;
3354
- document.getElementById("kwespay-explorerLink").href =
3355
- NETWORK_CONFIGS[this.state.selectedNetwork].explorer + receipt.hash;
3356
-
3357
- this._goToStep(5);
3358
-
3359
- dispatchWidgetEvent("paymentSuccess", {
3721
+ const onChainPayload = {
3360
3722
  transactionReference: receipt.transactionReference,
3361
3723
  paymentIdBytes32: receipt.paymentIdBytes32,
3362
3724
  transactionHash: receipt.hash,
3725
+ transactionStatus: "pending",
3363
3726
  fiatAmount: this.config.amount,
3364
3727
  currency: this.config.currency,
3365
3728
  token: this.state.selectedToken,
3366
3729
  network: this.state.selectedNetwork,
3730
+ };
3731
+
3732
+ dispatchWidgetEvent("paymentSuccess", onChainPayload);
3733
+ this.config.onPaymentSuccess?.(onChainPayload);
3734
+
3735
+ const confirmed = await this._awaitBackendConfirmation(receipt);
3736
+
3737
+ const decimals = this.state.selectedTokenConfig?.decimals ?? 18;
3738
+ const sym = this.state.selectedToken;
3739
+ const payload = this.state.currentPayload;
3740
+ const totalBig = BigInt(payload.totalBaseUnits);
3741
+ const cryptoDisplay = formatCryptoAmount(
3742
+ parseFloat(formatUnits(totalBig, decimals)),
3743
+ sym
3744
+ );
3745
+ const now = new Date();
3746
+ const timeString = now.toLocaleTimeString([], {
3747
+ hour: "2-digit",
3748
+ minute: "2-digit",
3367
3749
  });
3368
- } catch (error) {
3369
- console.error(
3370
- "[KwesPay] Payment error —",
3371
- error.code ?? "UNKNOWN",
3372
- ":",
3373
- error.message
3750
+ const dateString = now.toLocaleDateString([], {
3751
+ month: "short",
3752
+ day: "numeric",
3753
+ year: "numeric",
3754
+ });
3755
+
3756
+ const setEl = (id, val) => {
3757
+ const el = document.getElementById(id);
3758
+ if (el) el.textContent = val;
3759
+ };
3760
+
3761
+ setEl("kwespay-txHash", truncateHash(receipt.hash));
3762
+ setEl(
3763
+ "kwespay-txFiatAmount",
3764
+ `${this.config.amount} ${this.config.currency}`
3374
3765
  );
3766
+ setEl("kwespay-txCryptoAmount", cryptoDisplay);
3767
+ setEl("kwespay-txNetwork", this.state.selectedNetworkName);
3768
+ setEl("kwespay-txRef", receipt.transactionReference ?? "—");
3769
+ setEl("kwespay-txTime", `${dateString} · ${timeString}`);
3770
+
3771
+ const explorerLink = document.getElementById("kwespay-explorerLink");
3772
+ if (explorerLink) {
3773
+ explorerLink.href =
3774
+ (NETWORK_CONFIGS[this.state.selectedNetwork]?.explorer ?? "") +
3775
+ receipt.hash;
3776
+ }
3777
+
3778
+ this._setProcessingView("success");
3779
+ this._startReceiptCountdown(RECEIPT_DWELL_MS);
3780
+
3781
+ const finalStatus = confirmed?.transactionStatus ?? "completed";
3782
+ const finalPayload = {
3783
+ transactionReference: receipt.transactionReference,
3784
+ paymentIdBytes32: receipt.paymentIdBytes32,
3785
+ transactionHash: receipt.hash,
3786
+ transactionStatus: finalStatus,
3787
+ fiatAmount: this.config.amount,
3788
+ currency: this.config.currency,
3789
+ token: this.state.selectedToken,
3790
+ network: this.state.selectedNetwork,
3791
+ };
3375
3792
 
3793
+ this._finalisePayment(finalPayload, false);
3794
+ } catch (error) {
3376
3795
  document
3377
3796
  .getElementById("kwespay-mobileTransactionInstruction")
3378
3797
  ?.style.setProperty("display", "none");
@@ -3381,71 +3800,171 @@ const PaymentMethods = {
3381
3800
  let title = "Payment Failed";
3382
3801
  let message = getErrorMessage(error, { token: this.state.selectedToken });
3383
3802
 
3384
- if (error.code === "SESSION_EXPIRED") {
3385
- title = "Session Expired";
3386
- message = error.message;
3387
- } else if (error.code === "WRONG_NETWORK") {
3388
- title = "Wrong Network";
3389
- message = error.message;
3390
- } else if (errorType === "USER_REJECTED") {
3391
- title = "Transaction Cancelled";
3392
- message = "You rejected the transaction in your wallet.";
3393
- } else if (errorType === "INSUFFICIENT_BALANCE") {
3394
- title = "Insufficient Balance";
3803
+ switch (error.code) {
3804
+ case "SESSION_EXPIRED":
3805
+ title = "Session Expired";
3806
+ message = error.message;
3807
+ break;
3808
+ case "NO_PROVIDER":
3809
+ title = "Wallet Disconnected";
3810
+ message = error.message;
3811
+ break;
3812
+ case "NETWORK_ERROR":
3813
+ title = "Network Error";
3814
+ message = error.message;
3815
+ break;
3816
+ case "WRONG_NETWORK":
3817
+ title = "Wrong Network";
3818
+ message = error.message;
3819
+ break;
3820
+ case "MISSING_HASH":
3821
+ title = "Unknown Transaction Status";
3822
+ message = error.message;
3823
+ break;
3824
+ default:
3825
+ if (errorType === "USER_REJECTED") {
3826
+ title = "Transaction Cancelled";
3827
+ message = "You rejected the transaction in your wallet.";
3828
+ } else if (errorType === "INSUFFICIENT_BALANCE") {
3829
+ title = "Insufficient Balance";
3830
+ } else if (!message) {
3831
+ message =
3832
+ "An unexpected error occurred. Please try again or contact support.";
3833
+ }
3395
3834
  }
3396
3835
 
3836
+ console.error("[KwesPayWidget] Payment error:", {
3837
+ title,
3838
+ message,
3839
+ code: error.code,
3840
+ error,
3841
+ });
3842
+
3397
3843
  this._showError(title, message);
3398
- dispatchWidgetEvent("paymentError", { error: message, errorType });
3844
+ this._failPayment(message, errorType);
3399
3845
  }
3400
3846
  },
3401
3847
 
3402
- /**
3403
- * Confirm the wallet is on the target chain before sending a transaction.
3404
- * Retries up to 3 times with 1s delay to handle WC relay propagation lag.
3405
- * Throws WRONG_NETWORK if the chain never matches.
3406
- *
3407
- * MOBILE WC ONLY — never call this on desktop/injected.
3408
- */
3848
+ _setProcessingView(view) {
3849
+ ["processing", "confirming", "success"].forEach((v) => {
3850
+ const el = document.getElementById(`kwespay-view-${v}`);
3851
+ if (!el) return;
3852
+ el.style.display = v === view ? (v === "success" ? "flex" : "") : "none";
3853
+ });
3854
+
3855
+ const dot = document.getElementById("kwespay-step4-dot");
3856
+ const title = document.getElementById("kwespay-step4-title");
3857
+ const secure = document.getElementById("kwespay-step4-secure");
3858
+
3859
+ if (view === "success") {
3860
+ if (dot) {
3861
+ dot.style.background = "var(--kp-green)";
3862
+ dot.style.boxShadow = "0 0 8px rgba(16,185,129,0.4)";
3863
+ dot.style.animation = "none";
3864
+ }
3865
+ if (title) title.textContent = "Payment Complete";
3866
+ if (secure) secure.style.display = "flex";
3867
+ } else {
3868
+ if (dot) {
3869
+ dot.style.background = "var(--kp-accent)";
3870
+ dot.style.boxShadow = "0 0 8px var(--kp-accent-glow)";
3871
+ dot.style.animation = "";
3872
+ }
3873
+ if (title) title.textContent = "KwesPay Checkout";
3874
+ if (secure) secure.style.display = "none";
3875
+ }
3876
+ },
3877
+
3878
+ _startReceiptCountdown(totalMs) {
3879
+ this._stopReceiptCountdown();
3880
+
3881
+ const totalSec = Math.round(totalMs / 1000);
3882
+ let remaining = totalSec;
3883
+
3884
+ const countdownEl = document.getElementById("kwespay-receiptCountdown");
3885
+ const barEl = document.getElementById("kwespay-receiptCountdownBar");
3886
+
3887
+ const update = () => {
3888
+ if (countdownEl) {
3889
+ countdownEl.textContent =
3890
+ remaining > 0 ? `Closing in ${remaining}s` : "Closing…";
3891
+ }
3892
+ if (barEl) {
3893
+ const pct = (remaining / totalSec) * 100;
3894
+ barEl.style.width = `${pct}%`;
3895
+ barEl.style.background =
3896
+ remaining <= 3 ? "var(--kp-green)" : "var(--kp-accent)";
3897
+ }
3898
+ };
3899
+
3900
+ update();
3901
+
3902
+ this._receiptCountdownInterval = setInterval(() => {
3903
+ remaining -= 1;
3904
+ update();
3905
+ if (remaining <= 0) {
3906
+ this._stopReceiptCountdown();
3907
+ this.close();
3908
+ }
3909
+ }, 1000);
3910
+ },
3911
+
3912
+ _stopReceiptCountdown() {
3913
+ if (this._receiptCountdownInterval) {
3914
+ clearInterval(this._receiptCountdownInterval);
3915
+ this._receiptCountdownInterval = null;
3916
+ }
3917
+ },
3918
+
3919
+ _awaitBackendConfirmation(receipt) {
3920
+ return this.paymentService
3921
+ .pollTransactionStatus(receipt.transactionReference, {
3922
+ intervalMs: 4000,
3923
+ maxAttempts: 15,
3924
+ onStatus: (status) => {
3925
+ const el = document.getElementById("kwespay-confirmingText");
3926
+ if (el) el.textContent = `Network status: ${status}…`;
3927
+ },
3928
+ })
3929
+ .catch((err) => {
3930
+ dispatchWidgetEvent("paymentUnconfirmed", {
3931
+ transactionReference: receipt.transactionReference,
3932
+ transactionHash: receipt.hash,
3933
+ reason: err.message,
3934
+ });
3935
+ this.config.onPaymentUnconfirmed?.({
3936
+ transactionReference: receipt.transactionReference,
3937
+ transactionHash: receipt.hash,
3938
+ reason: err.message,
3939
+ });
3940
+ return { transactionStatus: "unconfirmed" };
3941
+ });
3942
+ },
3943
+
3409
3944
  async _assertMobileChain(provider, targetChainId) {
3410
3945
  const MAX_ATTEMPTS = 3;
3411
3946
  const DELAY_MS = 1000;
3412
3947
 
3413
3948
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
3414
3949
  let currentChainId = null;
3415
-
3416
3950
  try {
3417
3951
  const raw = await provider.request({ method: "eth_chainId" });
3418
3952
  currentChainId = parseInt(raw, 16);
3419
- } catch (err) {
3420
- console.error(
3421
- `[KwesPay] eth_chainId RPC failed (attempt ${attempt}/${MAX_ATTEMPTS}):`,
3422
- err.message
3423
- );
3424
- }
3425
-
3426
- console.log(
3427
- `[KwesPay] Chain check attempt ${attempt}/${MAX_ATTEMPTS} —`,
3428
- { currentChainId, targetChainId }
3429
- );
3430
-
3431
- if (currentChainId === targetChainId) {
3432
- console.log("[KwesPay] Chain confirmed ✅", currentChainId);
3433
- return;
3434
- }
3953
+ } catch {}
3435
3954
 
3436
- if (attempt < MAX_ATTEMPTS) {
3955
+ if (currentChainId === targetChainId) return;
3956
+ if (attempt < MAX_ATTEMPTS)
3437
3957
  await new Promise((r) => setTimeout(r, DELAY_MS));
3438
- }
3439
3958
  }
3440
3959
 
3441
- const err = new Error(
3442
- `Please switch to ${this.state.selectedNetworkName} in your wallet and try again.`
3960
+ throw Object.assign(
3961
+ new Error(
3962
+ `Please switch to ${this.state.selectedNetworkName} in your wallet and try again.`
3963
+ ),
3964
+ { code: "WRONG_NETWORK" }
3443
3965
  );
3444
- err.code = "WRONG_NETWORK";
3445
- throw err;
3446
3966
  },
3447
3967
 
3448
-
3449
3968
  async _switchNetworkSafe(
3450
3969
  chainId,
3451
3970
  networkName,
@@ -3453,9 +3972,7 @@ const PaymentMethods = {
3453
3972
  tokenSymbol,
3454
3973
  tokenDecimals
3455
3974
  ) {
3456
- const switchNetwork = this.walletService.switchNetwork;
3457
3975
  const provider = this.walletService.getProvider();
3458
-
3459
3976
  if (!provider) throw new Error("[WalletService] No provider connected");
3460
3977
 
3461
3978
  const toHex = (val) => {
@@ -3480,15 +3997,10 @@ const PaymentMethods = {
3480
3997
  await provider.request({ method: "eth_chainId" })
3481
3998
  );
3482
3999
  if (currentHex && currentHex === targetHex) return;
3483
- } catch (err) {
3484
- console.warn(
3485
- "[KwesPay] Could not read chainId before switch:",
3486
- err.message
3487
- );
3488
- }
4000
+ } catch {}
3489
4001
 
3490
4002
  try {
3491
- await switchNetwork(
4003
+ await this.walletService.switchNetwork(
3492
4004
  chainId,
3493
4005
  networkName,
3494
4006
  rpcUrl,
@@ -3496,15 +4008,9 @@ const PaymentMethods = {
3496
4008
  tokenDecimals
3497
4009
  );
3498
4010
  } catch (err) {
3499
- if (err.code === 4001) throw err; // user rejected
3500
- // Some wallets throw even on success — continue to verify below
3501
- console.warn(
3502
- "[KwesPay] switchNetwork threw (verifying anyway):",
3503
- err.message
3504
- );
4011
+ if (err.code === 4001) throw err;
3505
4012
  }
3506
4013
 
3507
- // Poll until confirmed or 15s timeout
3508
4014
  const POLL_MS = 500;
3509
4015
  const TIMEOUT_MS = 15_000;
3510
4016
  const started = Date.now();
@@ -3515,17 +4021,8 @@ const PaymentMethods = {
3515
4021
  const currentHex = toHex(
3516
4022
  await provider.request({ method: "eth_chainId" })
3517
4023
  );
3518
- if (currentHex && currentHex === targetHex) {
3519
- console.log(
3520
- `[KwesPay] Network switch confirmed after ${
3521
- Date.now() - started
3522
- }ms ✅`
3523
- );
3524
- return;
3525
- }
3526
- } catch (err) {
3527
- console.warn("[KwesPay] Poll eth_chainId error:", err.message);
3528
- }
4024
+ if (currentHex && currentHex === targetHex) return;
4025
+ } catch {}
3529
4026
  }
3530
4027
 
3531
4028
  throw new Error(
@@ -3543,6 +4040,14 @@ function resolveAcceptedTokens(input) {
3543
4040
  return null;
3544
4041
  }
3545
4042
 
4043
+ function formatFiatDisplay(amount) {
4044
+ const num = parseFloat(amount);
4045
+ if (isNaN(num)) return "0.00";
4046
+ return num % 1 === 0
4047
+ ? num.toString()
4048
+ : parseFloat(num.toPrecision(10)).toString();
4049
+ }
4050
+
3546
4051
  class KwesPayWidget {
3547
4052
  constructor(config) {
3548
4053
  if (!config.apiKey) throw new Error("[KwesPayWidget] apiKey is required");
@@ -3558,6 +4063,10 @@ class KwesPayWidget {
3558
4063
  currency: config.currency || DEFAULT_CONFIG.currency,
3559
4064
  graphqlEndpoint: DEFAULT_CONFIG.graphqlEndpoint,
3560
4065
  acceptedTokens: resolveAcceptedTokens(config.acceptedTokens),
4066
+ onPaymentSuccess: config.onPaymentSuccess ?? null,
4067
+ onPaymentConfirmed: config.onPaymentConfirmed ?? null,
4068
+ onPaymentUnconfirmed: config.onPaymentUnconfirmed ?? null,
4069
+ onPaymentError: config.onPaymentError ?? null,
3561
4070
  };
3562
4071
 
3563
4072
  if (!Object.values(SUPPORTED_CURRENCIES).includes(this.config.currency)) {
@@ -3590,21 +4099,61 @@ class KwesPayWidget {
3590
4099
  wcUri: null,
3591
4100
  };
3592
4101
 
4102
+ this._paymentResolve = null;
4103
+ this._paymentReject = null;
4104
+ this._finalised = false;
4105
+ this._receiptCountdownInterval = null;
4106
+
3593
4107
  this._init();
3594
4108
  }
3595
4109
 
3596
- async open() {
4110
+ get _displayAmount() {
4111
+ return formatFiatDisplay(this.config.amount);
4112
+ }
4113
+
4114
+ open() {
4115
+ this._finalised = false;
4116
+ this._paymentPromise = new Promise((resolve, reject) => {
4117
+ this._paymentResolve = resolve;
4118
+ this._paymentReject = reject;
4119
+ });
4120
+
3597
4121
  const overlay = document.getElementById("kwespay-widget-overlay");
3598
4122
  const container = document.getElementById("kwespay-widget-container");
3599
- if (!overlay || !container) return;
3600
-
3601
- container.classList.remove("closing");
3602
- overlay.classList.add("open");
3603
- document.body.classList.add("kwespay-open");
3604
- this.state.isOpen = true;
4123
+ if (overlay && container) {
4124
+ container.classList.remove("closing");
4125
+ overlay.classList.add("open");
4126
+ document.body.classList.add("kwespay-open");
4127
+ this.state.isOpen = true;
4128
+ }
3605
4129
 
3606
- await this._validateAPIKey();
4130
+ this._validateAPIKey();
3607
4131
  dispatchWidgetEvent("widgetOpened", {});
4132
+
4133
+ return this._paymentPromise;
4134
+ }
4135
+
4136
+ _finalisePayment(payload, autoClose = true) {
4137
+ if (this._finalised) return;
4138
+ this._finalised = true;
4139
+
4140
+ dispatchWidgetEvent("paymentConfirmed", payload);
4141
+ this.config.onPaymentConfirmed?.(payload);
4142
+
4143
+ this._paymentResolve?.(payload);
4144
+
4145
+ if (autoClose) {
4146
+ this.close();
4147
+ }
4148
+ }
4149
+
4150
+ _failPayment(message, errorType) {
4151
+ const errorPayload = { error: message, errorType };
4152
+ dispatchWidgetEvent("paymentError", errorPayload);
4153
+ this.config.onPaymentError?.(errorPayload);
4154
+
4155
+ const err = Object.assign(new Error(message), { code: errorType });
4156
+ this._paymentReject?.(err);
3608
4157
  }
3609
4158
 
3610
4159
  close() {
@@ -3612,28 +4161,29 @@ class KwesPayWidget {
3612
4161
  const container = document.getElementById("kwespay-widget-container");
3613
4162
  if (!overlay || !container) return;
3614
4163
 
4164
+ this._stopReceiptCountdown();
3615
4165
  this._clearQuoteTimer();
3616
- const closedAfterSuccess = this.state.currentStep === 5;
4166
+
4167
+ const closedAfterSuccess = this.state.currentStep === 4;
3617
4168
  const mobile = window.innerWidth <= 480;
3618
4169
 
3619
- if (mobile) {
3620
- container.classList.add("closing");
3621
- setTimeout(() => {
3622
- container.classList.remove("closing");
3623
- overlay.classList.remove("open");
3624
- document.body.classList.remove("kwespay-open");
3625
- this.state.isOpen = false;
3626
- dispatchWidgetEvent("widgetClosed", {
3627
- completedPayment: closedAfterSuccess,
3628
- });
3629
- }, 300);
3630
- } else {
4170
+ const finish = () => {
3631
4171
  overlay.classList.remove("open");
3632
4172
  document.body.classList.remove("kwespay-open");
3633
4173
  this.state.isOpen = false;
3634
4174
  dispatchWidgetEvent("widgetClosed", {
3635
4175
  completedPayment: closedAfterSuccess,
3636
4176
  });
4177
+ };
4178
+
4179
+ if (mobile) {
4180
+ container.classList.add("closing");
4181
+ setTimeout(() => {
4182
+ container.classList.remove("closing");
4183
+ finish();
4184
+ }, 300);
4185
+ } else {
4186
+ finish();
3637
4187
  }
3638
4188
  }
3639
4189
 
@@ -3651,12 +4201,13 @@ class KwesPayWidget {
3651
4201
  ) {
3652
4202
  this.config.currency = newCurrency;
3653
4203
  }
4204
+ const display = `${this._displayAmount} ${this.config.currency}`;
3654
4205
  document
3655
4206
  .querySelectorAll(
3656
4207
  '[id*="paymentAmount"], [id*="summaryFiatAmount"], [id*="txFiatAmount"], [id*="reviewFiatAmount"]'
3657
4208
  )
3658
4209
  .forEach((el) => {
3659
- el.textContent = `${this.config.amount} ${this.config.currency}`;
4210
+ el.textContent = display;
3660
4211
  });
3661
4212
  dispatchWidgetEvent("amountUpdated", {
3662
4213
  amount: this.config.amount,
@@ -3669,11 +4220,21 @@ class KwesPayWidget {
3669
4220
  }
3670
4221
 
3671
4222
  destroy() {
4223
+ this._stopReceiptCountdown();
3672
4224
  this._clearQuoteTimer();
3673
4225
  document.body.classList.remove("kwespay-open");
3674
4226
  this.walletService?.disconnect();
3675
4227
  document.getElementById("kwespay-widget-overlay")?.remove();
3676
4228
  document.getElementById("kwespay-widget-styles")?.remove();
4229
+
4230
+ if (!this._finalised) {
4231
+ this._paymentReject?.(
4232
+ Object.assign(new Error("Widget destroyed"), {
4233
+ code: "WIDGET_DESTROYED",
4234
+ })
4235
+ );
4236
+ }
4237
+
3677
4238
  this.state = null;
3678
4239
  this.config = null;
3679
4240
  this.walletService = null;
@@ -3693,9 +4254,103 @@ Object.assign(
3693
4254
  PaymentMethods
3694
4255
  );
3695
4256
 
4257
+ // Keyed by `${apiKey}::${vendorId}` so a page with multiple vendors/keys
4258
+ // each get their own widget instance, but a single vendor reuses one.
4259
+
4260
+ const _instances = new Map();
4261
+
4262
+ function _cacheKey(apiKey, vendorId) {
4263
+ return `${apiKey}::${vendorId}`;
4264
+ }
4265
+
4266
+ // kwespay()
4267
+
4268
+ /**
4269
+ * Open the KwesPay checkout widget and await the result.
4270
+ *
4271
+ * Handles the full lifecycle — construction, caching, amount updates,
4272
+ * open, and cleanup — so callers need nothing else.
4273
+ *
4274
+ * @param {object} config
4275
+ * @param {string} config.apiKey Your KwesPay public API key.
4276
+ * @param {string} config.vendorId Your vendor / merchant UUID.
4277
+ * @param {number} config.amount Fiat amount to charge (e.g. 49.99).
4278
+ * @param {string} [config.currency] ISO currency code (default: "USD").
4279
+ * @param {string[] | "stablecoins"} [config.acceptedTokens]
4280
+ * Restrict accepted crypto tokens.
4281
+ *
4282
+ * @returns {Promise<PaymentResult>}
4283
+ * Resolves when the payment is confirmed on-chain.
4284
+ * Rejects with an Error whose `.code` is one of:
4285
+ * "USER_CANCELLED" — user closed the widget without paying
4286
+ * "USER_REJECTED" — user rejected the wallet transaction
4287
+ * "SESSION_EXPIRED" — wallet session timed out mid-payment
4288
+ * "WIDGET_DESTROYED" — widget.destroy() was called externally
4289
+ * "UNKNOWN" — unexpected error (check err.message)
4290
+ *
4291
+ * @example
4292
+ * // Minimal usage — everything else is handled internally
4293
+ * try {
4294
+ * const result = await kwespay({
4295
+ * apiKey: "pk_...",
4296
+ * vendorId: "uuid",
4297
+ * amount: total,
4298
+ * currency: "USD",
4299
+ * });
4300
+ * console.log("Paid:", result.transactionHash);
4301
+ * } catch (err) {
4302
+ * if (err.code !== "USER_CANCELLED") console.error(err);
4303
+ * }
4304
+ */
4305
+ async function kwespay(config) {
4306
+ const { apiKey, vendorId, amount, currency, acceptedTokens } = config;
4307
+
4308
+ if (!apiKey) throw new Error("[kwespay] apiKey is required");
4309
+ if (!vendorId) throw new Error("[kwespay] vendorId is required");
4310
+ if (!amount || parseFloat(amount) <= 0)
4311
+ throw new Error("[kwespay] A valid amount is required");
4312
+
4313
+ const key = _cacheKey(apiKey, vendorId);
4314
+
4315
+ // Reuse existing instance or create a fresh one
4316
+ let widget = _instances.get(key);
4317
+
4318
+ if (!widget) {
4319
+ widget = new KwesPayWidget({
4320
+ apiKey,
4321
+ vendorId,
4322
+ amount,
4323
+ currency,
4324
+ acceptedTokens,
4325
+ });
4326
+ _instances.set(key, widget);
4327
+ } else {
4328
+ // Sync amount/currency in case they changed since last call
4329
+ widget.updateAmount(amount, currency);
4330
+ }
4331
+
4332
+ try {
4333
+ // open() returns a Promise that resolves/rejects when the payment settles.
4334
+ const result = await widget.open();
4335
+ return result;
4336
+ } finally {
4337
+ // Always evict after a settled payment or cancellation so the next call
4338
+ // starts fresh (new quote, clean state). The DOM is cleaned up by close(),
4339
+ // which open() calls internally on success; we just clear our cache ref.
4340
+ _instances.delete(key);
4341
+ }
4342
+ }
4343
+
3696
4344
  /**
3697
- * KwesPay Widget - Main entry point
3698
- * @module @kwespay/widget
4345
+ * @typedef {object} PaymentResult
4346
+ * @property {string} transactionHash On-chain tx hash.
4347
+ * @property {string} transactionReference KwesPay internal payment reference.
4348
+ * @property {string} paymentIdBytes32 On-chain payment ID (bytes32).
4349
+ * @property {string} transactionStatus "completed" | "unconfirmed"
4350
+ * @property {number} fiatAmount Charged fiat amount.
4351
+ * @property {string} currency ISO currency code.
4352
+ * @property {string} token Crypto token used (e.g. "USDC").
4353
+ * @property {string} network Chain used (e.g. "polygon").
3699
4354
  */
3700
4355
 
3701
- export { KwesPayWidget as default };
4356
+ export { KwesPayWidget, KwesPayWidget as default, kwespay };