@kwespay/widget 1.0.7 → 1.0.9

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://8284-154-161-98-26.ngrok-free.app",
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 = {
@@ -97,7 +140,6 @@ const TOKEN_CONFIGS = {
97
140
  decimals: 18,
98
141
  coingeckoId: "ethereum",
99
142
  binanceSymbol: "ETHUSDT",
100
-
101
143
  },
102
144
  {
103
145
  symbol: "USDT",
@@ -117,6 +159,15 @@ const TOKEN_CONFIGS = {
117
159
  coingeckoId: "usd-coin",
118
160
  binanceSymbol: "USDCUSDT",
119
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
+ },
120
171
  ],
121
172
  polygon: [
122
173
  {
@@ -137,6 +188,15 @@ const TOKEN_CONFIGS = {
137
188
  coingeckoId: "tether",
138
189
  binanceSymbol: "USDTUSDT",
139
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
+ },
140
200
  ],
141
201
  base: [
142
202
  {
@@ -157,38 +217,172 @@ const TOKEN_CONFIGS = {
157
217
  coingeckoId: "usd-coin",
158
218
  binanceSymbol: "USDCUSDT",
159
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
+ },
160
229
  ],
161
230
  lisk: [
162
231
  {
163
232
  symbol: "ETH",
164
- name: "Lisk",
165
- icon: "https://arthuremma2.github.io/img-hosting/liskt.png",
233
+ name: "Ethereum",
234
+ icon: "https://arthuremma2.github.io/img-hosting/ethereum-eth.svg",
166
235
  address: "0x0000000000000000000000000000000000000000",
167
236
  decimals: 18,
168
237
  coingeckoId: "ethereum",
169
238
  binanceSymbol: "ETHUSDT",
170
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
+ },
171
249
  ],
172
- 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: [
173
280
  {
174
281
  symbol: "ETH",
175
- name: "Sepolia ETH",
282
+ name: "Ethereum",
176
283
  icon: "https://arthuremma2.github.io/img-hosting/ethereum-eth.svg",
177
284
  address: "0x0000000000000000000000000000000000000000",
178
285
  decimals: 18,
179
286
  coingeckoId: "ethereum",
180
287
  binanceSymbol: "ETHUSDT",
181
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
+ },
182
298
  {
183
299
  symbol: "USDT",
184
- name: "Mock USDT",
300
+ name: "Tether USD",
185
301
  icon: "https://move-flow.github.io/assets/tether-usdt-logo.svg",
186
- 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",
187
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,
188
333
  coingeckoId: "tether",
189
334
  binanceSymbol: "USDTUSDT",
190
335
  },
191
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
+ ],
192
386
  polygonAmoy: [
193
387
  {
194
388
  symbol: "MATIC",
@@ -200,13 +394,13 @@ const TOKEN_CONFIGS = {
200
394
  binanceSymbol: "MATICUSDT",
201
395
  },
202
396
  {
203
- symbol: "USDc",
204
- name: "Mock USDT",
205
- 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",
206
400
  address: "0x8B0180f2101c8260d49339abfEe87927412494B4",
207
401
  decimals: 6,
208
- coingeckoId: "tether",
209
- binanceSymbol: "USDTUSDT",
402
+ coingeckoId: "usd-coin",
403
+ binanceSymbol: "USDCUSDT",
210
404
  },
211
405
  ],
212
406
  baseSepolia: [
@@ -221,7 +415,7 @@ const TOKEN_CONFIGS = {
221
415
  },
222
416
  {
223
417
  symbol: "USDC",
224
- name: "USD Coin",
418
+ name: "Mock USDC",
225
419
  icon: "https://move-flow.github.io/assets/usd-coin-usdc-logo.svg",
226
420
  address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
227
421
  decimals: 6,
@@ -229,24 +423,33 @@ const TOKEN_CONFIGS = {
229
423
  binanceSymbol: "USDCUSDT",
230
424
  },
231
425
  ],
232
- liskTestnet: [
426
+ arbitrumSepolia: [
233
427
  {
234
428
  symbol: "ETH",
235
- name: "Lisk Sepolia",
236
- 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",
237
431
  address: "0x0000000000000000000000000000000000000000",
238
432
  decimals: 18,
239
433
  coingeckoId: "ethereum",
240
434
  binanceSymbol: "ETHUSDT",
241
435
  },
242
436
  {
243
- symbol: "LSK",
244
- name: "Lisk",
245
- icon: "https://arthuremma2.github.io/img-hosting/liskt.png",
246
- 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",
247
450
  decimals: 18,
248
- coingeckoId: "LSK",
249
- binanceSymbol: "LSK",
451
+ coingeckoId: "arbitrum",
452
+ binanceSymbol: "ARBUSDT",
250
453
  },
251
454
  ],
252
455
  };
@@ -810,36 +1013,36 @@ class PaymentService {
810
1013
  constructor(apiKey, graphqlEndpoint) {
811
1014
  this.apiKey = apiKey;
812
1015
  this.graphqlEndpoint = graphqlEndpoint;
1016
+ this._graphqlEndpoint = graphqlEndpoint;
1017
+ this._apiKey = apiKey;
813
1018
  this.client = null;
814
-
815
- console.log("[KwesPay] PaymentService initialized", {
816
- apiKey: this.apiKey?.slice(0, 6) + "...",
817
- });
818
1019
  }
819
1020
 
820
1021
  async _initClient() {
821
1022
  if (this.client) return;
822
-
823
- console.log("[KwesPay] Initializing SDK client...");
824
-
825
- const { KwesPayClient } = await import('./index-338l55OY.js');
826
-
1023
+ const { KwesPayClient } = await import('./index-BDgXWGKX.js');
827
1024
  this.client = new KwesPayClient({ apiKey: this.apiKey });
1025
+ }
828
1026
 
829
- 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();
830
1037
  }
831
1038
 
832
1039
  async validateAPIKey() {
833
1040
  try {
834
1041
  await this._initClient();
835
-
836
1042
  const result = await this.client.validateKey();
837
-
838
1043
  if (!result.isValid) {
839
- console.error("[KwesPay] Invalid API key:", result.error);
840
1044
  return { valid: false, error: result.error ?? "Invalid access key" };
841
1045
  }
842
-
843
1046
  return {
844
1047
  valid: true,
845
1048
  keyId: result.keyId,
@@ -856,94 +1059,94 @@ class PaymentService {
856
1059
 
857
1060
  async getQuote(params) {
858
1061
  await this._initClient();
859
-
860
- console.log("[KwesPay] Requesting quote...", params);
861
-
862
- const quote = await this.client.quote({
863
- vendorIdentifier: params.vendorId,
1062
+ return this.client.getQuote({
1063
+ vendorIdentifier: params.vendorIdentifier,
864
1064
  fiatAmount: params.fiatAmount,
865
1065
  fiatCurrency: params.fiatCurrency || "USD",
866
1066
  cryptoCurrency: params.cryptoCurrency,
867
1067
  network: params.network,
868
- payerWalletAddress: params.payerWalletAddress,
869
1068
  });
870
-
871
- console.log("[KwesPay] Quote received:", quote);
872
-
873
- return quote;
874
1069
  }
875
1070
 
876
1071
  async createPayment({ payload, walletProvider, onStatusUpdate }) {
877
1072
  await this._initClient();
878
1073
 
879
- console.log("[KwesPay] 💳 createPayment called", {
880
- payloadKeys: payload ? Object.keys(payload) : null,
881
- amountBaseUnits: payload?.amountBaseUnits,
882
- contractAddress: payload?.contractAddress,
883
- paymentId: payload?.paymentId,
884
- vendorAddress: payload?.vendorAddress,
885
- expiresAt: payload?.expiresAt,
886
- providerType: walletProvider?.constructor?.name,
887
- });
888
-
889
- try {
890
- const result = await this.client.pay({
891
- provider: walletProvider,
892
- payload,
893
- onStatus: (status) => {
894
- console.log("[KwesPay] 📋 Payment status update:", status);
895
- onStatusUpdate?.(status);
896
- },
897
- });
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
+ );
898
1099
 
899
- console.log("[KwesPay] Payment completed:", result);
1100
+ const ct = rawTx?.data?.createTransaction;
900
1101
 
901
- return {
902
- hash: result.txHash,
903
- blockNumber: result.blockNumber,
904
- transactionReference: result.transactionReference,
905
- paymentIdBytes32: result.paymentIdBytes32,
906
- };
907
- } catch (err) {
908
- console.error("[KwesPay] ❌ createPayment error:", {
909
- message: err?.message,
910
- code: err?.code,
911
- reason: err?.reason,
912
- data: err?.data,
913
- transaction: err?.transaction
914
- ? {
915
- to: err.transaction?.to,
916
- from: err.transaction?.from,
917
- value: err.transaction?.value?.toString(),
918
- gasLimit: err.transaction?.gasLimit?.toString(),
919
- data: err.transaction?.data,
920
- }
921
- : undefined,
922
- receipt: err?.receipt
923
- ? {
924
- status: err.receipt?.status,
925
- gasUsed: err.receipt?.gasUsed?.toString(),
926
- blockNumber: err.receipt?.blockNumber,
927
- transactionHash: err.receipt?.transactionHash,
928
- }
929
- : undefined,
930
- stack: err?.stack,
931
- raw: err,
932
- });
933
- throw err;
1102
+ if (!ct?.success) {
1103
+ throw new Error(ct?.message ?? "Transaction creation failed");
934
1104
  }
935
- }
936
1105
 
937
- async getTransactionStatus(transactionReference) {
938
- await this._initClient();
1106
+ if (!ct.deadline) {
1107
+ throw new Error("Backend did not return a deadline");
1108
+ }
939
1109
 
940
- 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
+ };
941
1132
 
942
- 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
+ });
943
1138
 
944
- 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
+ }
945
1146
 
946
- return status;
1147
+ async getTransactionStatus(transactionReference) {
1148
+ await this._initClient();
1149
+ return this.client.getTransactionStatus(transactionReference);
947
1150
  }
948
1151
 
949
1152
  async pollTransactionStatus(
@@ -951,28 +1154,13 @@ class PaymentService {
951
1154
  { onStatus, intervalMs = 4000, maxAttempts = 60 } = {}
952
1155
  ) {
953
1156
  await this._initClient();
954
-
955
- console.log("[KwesPay] Starting polling...", {
956
- transactionReference,
957
- intervalMs,
958
- });
959
-
960
1157
  let attempts = 0;
961
-
962
1158
  return new Promise((resolve, reject) => {
963
1159
  const id = setInterval(async () => {
964
1160
  attempts++;
965
-
966
1161
  try {
967
1162
  const status = await this.getTransactionStatus(transactionReference);
968
-
969
- console.log(
970
- `[KwesPay] Poll attempt ${attempts}:`,
971
- status.transactionStatus
972
- );
973
-
974
1163
  onStatus?.(status.transactionStatus);
975
-
976
1164
  const terminal = [
977
1165
  "completed",
978
1166
  "failed",
@@ -981,22 +1169,14 @@ class PaymentService {
981
1169
  "overpaid",
982
1170
  "refunded",
983
1171
  ];
984
-
985
1172
  if (terminal.includes(status.transactionStatus)) {
986
- console.log(
987
- "[KwesPay] Final status reached:",
988
- status.transactionStatus
989
- );
990
1173
  clearInterval(id);
991
1174
  resolve(status);
992
1175
  } else if (attempts >= maxAttempts) {
993
- console.error("[KwesPay] Polling timeout");
994
1176
  clearInterval(id);
995
1177
  reject(new Error("Transaction status polling timed out."));
996
1178
  }
997
1179
  } catch (err) {
998
- console.error("[KwesPay] Polling error:", err);
999
-
1000
1180
  if (attempts >= maxAttempts) {
1001
1181
  clearInterval(id);
1002
1182
  reject(err);
@@ -1027,6 +1207,18 @@ function truncateHash(hash, startChars = 10, endChars = 8) {
1027
1207
  return `${hash.slice(0, startChars)}...${hash.slice(-endChars)}`;
1028
1208
  }
1029
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
+
1030
1222
  function getErrorType(error) {
1031
1223
  const msg = error?.message ?? "";
1032
1224
  if (
@@ -1091,20 +1283,20 @@ const WIDGET_STYLES = `
1091
1283
  * { margin: 0; padding: 0; box-sizing: border-box; }
1092
1284
 
1093
1285
  :root {
1094
- --kp-bg: #0a0a0f;
1095
- --kp-surface: #0f0f18;
1096
- --kp-surface-2: #16161f;
1097
- --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);
1098
1290
  --kp-border-active: rgba(99,102,241,0.5);
1099
- --kp-accent: #6366f1;
1100
- --kp-accent-dim: rgba(99,102,241,0.1);
1101
- --kp-accent-glow: rgba(99,102,241,0.25);
1102
- --kp-green: #10b981;
1103
- --kp-red: #f43f5e;
1104
- --kp-text: #f1f0ff;
1105
- --kp-muted: #6b6a80;
1106
- --kp-mono: 'Inter', monospace;
1107
- --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;
1108
1300
  }
1109
1301
 
1110
1302
  body.kwespay-open {
@@ -1129,19 +1321,17 @@ const WIDGET_STYLES = `
1129
1321
 
1130
1322
  .kwespay-close-btn {
1131
1323
  position: absolute;
1132
- top: 20px; right: 20px;
1324
+ top: 12px; right: 12px;
1133
1325
  background: rgba(255,255,255,0.06);
1134
1326
  border: 1px solid var(--kp-border);
1135
1327
  color: var(--kp-muted);
1136
- width: 34px; height: 34px;
1137
- border-radius: 8px;
1328
+ width: 28px; height: 28px;
1329
+ border-radius: 7px;
1138
1330
  cursor: pointer;
1139
- display: flex;
1140
- align-items: center;
1141
- justify-content: center;
1142
- font-size: 18px;
1331
+ display: flex; align-items: center; justify-content: center;
1332
+ font-size: 16px;
1143
1333
  transition: all 0.15s;
1144
- z-index: 1000000;
1334
+ z-index: 10;
1145
1335
  }
1146
1336
 
1147
1337
  .kwespay-close-btn:hover {
@@ -1150,6 +1340,12 @@ const WIDGET_STYLES = `
1150
1340
  border-color: var(--kp-border-active);
1151
1341
  }
1152
1342
 
1343
+ .kwespay-steps-wrapper {
1344
+ position: relative;
1345
+ flex: 1;
1346
+ overflow: hidden;
1347
+ }
1348
+
1153
1349
  .material-symbols-outlined {
1154
1350
  font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
1155
1351
  }
@@ -1208,6 +1404,8 @@ const WIDGET_STYLES = `
1208
1404
  to { opacity: 0; }
1209
1405
  }
1210
1406
 
1407
+
1408
+
1211
1409
  .kp-topbar {
1212
1410
  display: flex;
1213
1411
  align-items: center;
@@ -1224,11 +1422,8 @@ const WIDGET_STYLES = `
1224
1422
  }
1225
1423
 
1226
1424
  .kp-back-btn {
1227
- display: flex;
1228
- align-items: center;
1229
- justify-content: center;
1230
- width: 28px;
1231
- height: 28px;
1425
+ display: flex; align-items: center; justify-content: center;
1426
+ width: 28px; height: 28px;
1232
1427
  border-radius: 7px;
1233
1428
  background: rgba(255,255,255,0.04);
1234
1429
  border: 1px solid var(--kp-border);
@@ -1253,11 +1448,12 @@ const WIDGET_STYLES = `
1253
1448
  background: var(--kp-accent);
1254
1449
  box-shadow: 0 0 8px var(--kp-accent-glow);
1255
1450
  animation: kpPulse 2s ease infinite;
1451
+ flex-shrink: 0;
1256
1452
  }
1257
1453
 
1258
1454
  @keyframes kpPulse {
1259
1455
  0%, 100% { opacity: 1; }
1260
- 50% { opacity: 0.5; }
1456
+ 50% { opacity: 0.4; }
1261
1457
  }
1262
1458
 
1263
1459
  .kp-topbar-name {
@@ -1268,9 +1464,7 @@ const WIDGET_STYLES = `
1268
1464
  }
1269
1465
 
1270
1466
  .kp-topbar-secure {
1271
- display: flex;
1272
- align-items: center;
1273
- gap: 5px;
1467
+ display: flex; align-items: center; gap: 5px;
1274
1468
  font-family: var(--kp-mono);
1275
1469
  font-size: 10px;
1276
1470
  color: var(--kp-muted);
@@ -1278,10 +1472,8 @@ const WIDGET_STYLES = `
1278
1472
  font-weight: 500;
1279
1473
  }
1280
1474
 
1281
- .kp-topbar-secure .material-symbols-outlined {
1282
- font-size: 13px;
1283
- color: var(--kp-green);
1284
- }
1475
+ .kp-topbar-secure .material-symbols-outlined { font-size: 13px; color: var(--kp-green); }
1476
+
1285
1477
 
1286
1478
  .kp-amount-block {
1287
1479
  padding: 20px 20px 16px;
@@ -1328,6 +1520,8 @@ const WIDGET_STYLES = `
1328
1520
 
1329
1521
  .kp-amount-crypto.loading { opacity: 0.4; }
1330
1522
 
1523
+ /* ── Progress ───────────────────────────────────────────────────────────── */
1524
+
1331
1525
  .progress-section {
1332
1526
  padding: 14px 20px 12px;
1333
1527
  border-bottom: 1px solid var(--kp-border);
@@ -1335,9 +1529,7 @@ const WIDGET_STYLES = `
1335
1529
  }
1336
1530
 
1337
1531
  .progress-info {
1338
- display: flex;
1339
- align-items: center;
1340
- justify-content: space-between;
1532
+ display: flex; align-items: center; justify-content: space-between;
1341
1533
  margin-bottom: 8px;
1342
1534
  }
1343
1535
 
@@ -1360,8 +1552,7 @@ const WIDGET_STYLES = `
1360
1552
  .progress-bars { display: flex; width: 100%; gap: 5px; }
1361
1553
 
1362
1554
  .progress-bar {
1363
- height: 2px;
1364
- flex: 1;
1555
+ height: 2px; flex: 1;
1365
1556
  border-radius: 999px;
1366
1557
  background: var(--kp-surface-2);
1367
1558
  transition: background 0.3s ease;
@@ -1372,6 +1563,8 @@ const WIDGET_STYLES = `
1372
1563
  box-shadow: 0 0 8px var(--kp-accent-glow);
1373
1564
  }
1374
1565
 
1566
+ /* ── Scrollable content ─────────────────────────────────────────────────── */
1567
+
1375
1568
  .section-hint {
1376
1569
  font-size: 12px;
1377
1570
  color: var(--kp-muted);
@@ -1395,10 +1588,10 @@ const WIDGET_STYLES = `
1395
1588
  border-radius: 10px;
1396
1589
  }
1397
1590
 
1591
+ /* ── Network list ───────────────────────────────────────────────────────── */
1592
+
1398
1593
  .item-list {
1399
- display: flex;
1400
- flex-direction: column;
1401
- gap: 6px;
1594
+ display: flex; flex-direction: column; gap: 6px;
1402
1595
  margin-bottom: 6px;
1403
1596
  }
1404
1597
 
@@ -1417,9 +1610,7 @@ const WIDGET_STYLES = `
1417
1610
  }
1418
1611
 
1419
1612
  .list-item {
1420
- display: flex;
1421
- align-items: center;
1422
- gap: 12px;
1613
+ display: flex; align-items: center; gap: 12px;
1423
1614
  background: var(--kp-surface);
1424
1615
  padding: 12px 14px;
1425
1616
  border-radius: 12px;
@@ -1444,15 +1635,11 @@ const WIDGET_STYLES = `
1444
1635
  flex-shrink: 0;
1445
1636
  }
1446
1637
 
1447
- .item-icon img { width: 22px; height: 22px; object-fit: contain; }
1638
+ .item-icon img { width: 28px; height: 28px; object-fit: contain; }
1448
1639
  .item-info { display: flex; flex-direction: column; flex: 1; }
1449
1640
  .item-name-row { display: flex; align-items: center; gap: 7px; }
1450
1641
 
1451
- .item-name {
1452
- font-size: 13px;
1453
- font-weight: 600;
1454
- color: var(--kp-text);
1455
- }
1642
+ .item-name { font-size: 13px; font-weight: 600; color: var(--kp-text); }
1456
1643
 
1457
1644
  .item-badge {
1458
1645
  padding: 1px 6px;
@@ -1480,10 +1667,10 @@ const WIDGET_STYLES = `
1480
1667
 
1481
1668
  .item-chevron { color: var(--kp-muted); font-size: 16px; }
1482
1669
 
1670
+ /* ── Token list ─────────────────────────────────────────────────────────── */
1671
+
1483
1672
  .token-item {
1484
- display: flex;
1485
- align-items: center;
1486
- gap: 12px;
1673
+ display: flex; align-items: center; gap: 12px;
1487
1674
  padding: 13px 14px;
1488
1675
  cursor: pointer;
1489
1676
  transition: all 0.15s;
@@ -1496,14 +1683,24 @@ const WIDGET_STYLES = `
1496
1683
  }
1497
1684
 
1498
1685
  .token-item:last-child { margin-bottom: 0; }
1499
- .token-item:hover { border-color: var(--kp-border-active); background: var(--kp-accent-dim); }
1500
- .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
+
1501
1697
  .token-item.selected::after {
1502
1698
  content: '';
1503
1699
  position: absolute; inset: 0;
1504
1700
  background: linear-gradient(90deg, rgba(99,102,241,0.08) 0%, transparent 100%);
1505
1701
  pointer-events: none;
1506
1702
  }
1703
+
1507
1704
  .token-item.selected .token-symbol { color: var(--kp-accent); }
1508
1705
 
1509
1706
  .token-left { display: flex; align-items: center; gap: 12px; flex: 1; }
@@ -1513,11 +1710,8 @@ const WIDGET_STYLES = `
1513
1710
  border-radius: 10px;
1514
1711
  background: var(--kp-surface-2);
1515
1712
  border: 1px solid var(--kp-border);
1516
- overflow: hidden;
1517
- flex-shrink: 0;
1518
- display: flex;
1519
- align-items: center;
1520
- justify-content: center;
1713
+ overflow: hidden; flex-shrink: 0;
1714
+ display: flex; align-items: center; justify-content: center;
1521
1715
  }
1522
1716
 
1523
1717
  .token-icon img { width: 22px; height: 22px; object-fit: contain; }
@@ -1543,6 +1737,8 @@ const WIDGET_STYLES = `
1543
1737
  .token-chevron { color: var(--kp-muted); font-size: 16px; flex-shrink: 0; }
1544
1738
  #kwespay-tokenList { display: flex; flex-direction: column; }
1545
1739
 
1740
+ /* ── Buttons ────────────────────────────────────────────────────────────── */
1741
+
1546
1742
  .bottom-action {
1547
1743
  padding: 12px 20px 16px;
1548
1744
  background: var(--kp-bg);
@@ -1584,6 +1780,7 @@ const WIDGET_STYLES = `
1584
1780
  }
1585
1781
 
1586
1782
  .action-btn.secondary::before { display: none; }
1783
+
1587
1784
  .action-btn.secondary:hover {
1588
1785
  background: var(--kp-surface-2);
1589
1786
  border-color: var(--kp-border-active);
@@ -1593,14 +1790,13 @@ const WIDGET_STYLES = `
1593
1790
 
1594
1791
  .action-btn:disabled { opacity: 0.3; cursor: not-allowed; box-shadow: none; }
1595
1792
 
1793
+ /* ── Footer ─────────────────────────────────────────────────────────────── */
1794
+
1596
1795
  .kp-footer {
1597
1796
  padding: 10px 20px 12px;
1598
1797
  background: var(--kp-bg);
1599
1798
  border-top: 1px solid var(--kp-border);
1600
- display: flex;
1601
- align-items: center;
1602
- justify-content: center;
1603
- gap: 6px;
1799
+ display: flex; align-items: center; justify-content: center; gap: 6px;
1604
1800
  flex-shrink: 0;
1605
1801
  }
1606
1802
 
@@ -1616,11 +1812,11 @@ const WIDGET_STYLES = `
1616
1812
 
1617
1813
  .kp-footer-text span { color: var(--kp-text); font-weight: 500; }
1618
1814
 
1815
+ /* ── Spinner / loading ──────────────────────────────────────────────────── */
1816
+
1619
1817
  .loading-container {
1620
- display: flex;
1621
- flex-direction: column;
1622
- align-items: center;
1623
- justify-content: center;
1818
+ display: flex; flex-direction: column;
1819
+ align-items: center; justify-content: center;
1624
1820
  padding: 24px 20px;
1625
1821
  flex: 1;
1626
1822
  }
@@ -1650,6 +1846,21 @@ const WIDGET_STYLES = `
1650
1846
  animation: kpSpin 1.2s linear infinite reverse;
1651
1847
  }
1652
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
+
1653
1864
  @keyframes kpSpin { to { transform: rotate(360deg); } }
1654
1865
 
1655
1866
  .spinner-icon {
@@ -1683,6 +1894,39 @@ const WIDGET_STYLES = `
1683
1894
  font-weight: 400;
1684
1895
  }
1685
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
+
1686
1930
  .success-icon {
1687
1931
  width: 64px; height: 64px;
1688
1932
  border-radius: 16px;
@@ -1694,6 +1938,8 @@ const WIDGET_STYLES = `
1694
1938
 
1695
1939
  .success-icon .material-symbols-outlined { font-size: 36px; color: var(--kp-green); }
1696
1940
 
1941
+ /* ── Error icon + hint ──────────────────────────────────────────────────── */
1942
+
1697
1943
  .error-icon {
1698
1944
  width: 64px; height: 64px;
1699
1945
  border-radius: 16px;
@@ -1705,12 +1951,28 @@ const WIDGET_STYLES = `
1705
1951
 
1706
1952
  .error-icon .material-symbols-outlined { font-size: 36px; color: var(--kp-red); }
1707
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
+
1708
1972
  .network-status { padding: 14px 20px 0; }
1709
1973
 
1710
1974
  .status-card {
1711
- display: flex;
1712
- align-items: center;
1713
- gap: 10px;
1975
+ display: flex; align-items: center; gap: 10px;
1714
1976
  background: var(--kp-accent-dim);
1715
1977
  border: 1px solid rgba(99,102,241,0.2);
1716
1978
  border-radius: 10px;
@@ -1728,6 +1990,8 @@ const WIDGET_STYLES = `
1728
1990
  .status-icon img { width: 16px; height: 16px; object-fit: contain; }
1729
1991
  .status-text { color: var(--kp-accent); font-size: 12px; font-weight: 600; flex: 1; }
1730
1992
 
1993
+ /* ── Review body ────────────────────────────────────────────────────────── */
1994
+
1731
1995
  .kp-review-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
1732
1996
  .kp-review-body::-webkit-scrollbar { width: 3px; }
1733
1997
  .kp-review-body::-webkit-scrollbar-thumb { background: rgba(99,102,241,0.2); border-radius: 10px; }
@@ -1759,6 +2023,8 @@ const WIDGET_STYLES = `
1759
2023
 
1760
2024
  .kp-review-crypto-line.loading { opacity: 0.4; }
1761
2025
 
2026
+ /* ── Detail / fee blocks ────────────────────────────────────────────────── */
2027
+
1762
2028
  .kp-detail-block {
1763
2029
  background: var(--kp-surface);
1764
2030
  border: 1px solid var(--kp-border);
@@ -1768,9 +2034,7 @@ const WIDGET_STYLES = `
1768
2034
  }
1769
2035
 
1770
2036
  .kp-detail-row {
1771
- display: flex;
1772
- justify-content: space-between;
1773
- align-items: center;
2037
+ display: flex; justify-content: space-between; align-items: center;
1774
2038
  padding: 11px 16px;
1775
2039
  border-bottom: 1px solid var(--kp-border);
1776
2040
  }
@@ -1794,7 +2058,7 @@ const WIDGET_STYLES = `
1794
2058
  }
1795
2059
 
1796
2060
  .kp-detail-val.accent { color: var(--kp-accent); }
1797
- .kp-detail-val.green { color: var(--kp-green); }
2061
+ .kp-detail-val.green { color: var(--kp-green); }
1798
2062
 
1799
2063
  .kp-fee-block {
1800
2064
  background: rgba(16,185,129,0.04);
@@ -1824,6 +2088,8 @@ const WIDGET_STYLES = `
1824
2088
 
1825
2089
  .kp-fee-header .material-symbols-outlined { font-size: 13px; color: var(--kp-green); }
1826
2090
 
2091
+ /* ── Transaction receipt ────────────────────────────────────────────────── */
2092
+
1827
2093
  .tx-details {
1828
2094
  width: 100%;
1829
2095
  background: var(--kp-surface);
@@ -1834,17 +2100,15 @@ const WIDGET_STYLES = `
1834
2100
  }
1835
2101
 
1836
2102
  .tx-row {
1837
- display: flex;
1838
- justify-content: space-between;
1839
- align-items: center;
2103
+ display: flex; justify-content: space-between; align-items: center;
1840
2104
  padding: 10px 16px;
1841
2105
  border-bottom: 1px solid var(--kp-border);
1842
2106
  }
1843
2107
 
1844
2108
  .tx-row:last-child { border-bottom: none; }
1845
2109
 
1846
- .tx-label { font-family: var(--kp-mono); color: var(--kp-muted); font-size: 10px; letter-spacing: 0.02em; font-weight: 400; }
1847
- .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); }
1848
2112
  .tx-hash-row { display: flex; align-items: center; gap: 8px; }
1849
2113
 
1850
2114
  .explorer-link {
@@ -1861,26 +2125,57 @@ const WIDGET_STYLES = `
1861
2125
  .explorer-link:hover { background: rgba(99,102,241,0.2); }
1862
2126
  .explorer-link .material-symbols-outlined { font-size: 12px; color: var(--kp-accent); }
1863
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
+
1864
2161
  .mobile-instruction {
1865
2162
  background: var(--kp-accent-dim);
1866
2163
  border: 1px solid rgba(99,102,241,0.2);
1867
2164
  border-radius: 10px;
1868
2165
  padding: 12px;
1869
2166
  margin: 12px 0 0;
1870
- display: flex;
1871
- align-items: flex-start;
1872
- gap: 10px;
2167
+ display: flex; align-items: flex-start; gap: 10px;
1873
2168
  }
1874
2169
 
1875
2170
  .mobile-instruction-icon { color: var(--kp-accent); font-size: 18px; flex-shrink: 0; margin-top: 1px; }
1876
2171
  .mobile-instruction-text { flex: 1; }
1877
2172
  .mobile-instruction-title { color: var(--kp-text); font-size: 12px; font-weight: 600; margin-bottom: 3px; }
1878
- .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 ────────────────────────────────────────────────────────── */
1879
2176
 
1880
2177
  .kp-quote-timer {
1881
- display: flex;
1882
- align-items: center;
1883
- gap: 5px;
2178
+ display: flex; align-items: center; gap: 5px;
1884
2179
  font-family: var(--kp-mono);
1885
2180
  font-size: 10px;
1886
2181
  color: var(--kp-muted);
@@ -1889,9 +2184,33 @@ const WIDGET_STYLES = `
1889
2184
  }
1890
2185
 
1891
2186
  .kp-quote-timer .material-symbols-outlined { font-size: 12px; }
1892
- .kp-quote-timer.urgent { color: #fb923c; }
2187
+ .kp-quote-timer.urgent { color: #fb923c; }
1893
2188
  .kp-quote-timer.expired { color: var(--kp-red); }
1894
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
+
2213
+
1895
2214
  @media (max-width: 480px) {
1896
2215
  .kwespay-overlay {
1897
2216
  padding: 0;
@@ -1916,79 +2235,38 @@ const WIDGET_STYLES = `
1916
2235
  animation: kpSheetDown 0.28s ease forwards;
1917
2236
  }
1918
2237
 
1919
- .kwespay-container::before {
1920
- content: '';
1921
- display: block;
1922
- position: absolute;
1923
- top: 8px; left: 50%;
1924
- transform: translateX(-50%);
1925
- width: 28px; height: 3px;
1926
- border-radius: 2px;
1927
- background: rgba(255,255,255,0.12);
1928
- z-index: 10;
1929
- }
1930
- }
1931
-
1932
-
1933
- .kp-mobile-connect-status {
1934
- display: flex;
1935
- align-items: center;
1936
- gap: 12px;
1937
- background: var(--kp-accent-dim);
1938
- border: 1px solid rgba(99,102,241,0.2);
1939
- border-radius: 12px;
1940
- padding: 14px 16px;
1941
- margin-bottom: 4px;
1942
- }
1943
-
1944
- .kp-mobile-status-icon {
1945
- width: 28px; height: 28px;
1946
- display: flex; align-items: center; justify-content: center;
1947
- flex-shrink: 0;
1948
- }
1949
-
1950
- .kp-mobile-status-text { flex: 1; }
1951
-
1952
- .kp-mobile-status-title {
1953
- font-size: 13px;
1954
- font-weight: 600;
1955
- color: var(--kp-text);
1956
- margin-bottom: 2px;
1957
- }
1958
-
1959
- .kp-mobile-status-desc {
1960
- font-family: var(--kp-mono);
1961
- font-size: 11px;
1962
- color: var(--kp-muted);
1963
- }
1964
-
1965
- .kp-wallet-option:active {
1966
- transform: scale(0.98);
1967
- background: var(--kp-accent-dim);
1968
- border-color: var(--kp-border-active);
1969
- }
1970
-
2238
+ .kwespay-container::before {
2239
+ content: '';
2240
+ display: block;
2241
+ position: absolute;
2242
+ top: 8px; left: 50%;
2243
+ transform: translateX(-50%);
2244
+ width: 28px; height: 3px;
2245
+ border-radius: 2px;
2246
+ background: rgba(255,255,255,0.12);
2247
+ z-index: 10;
2248
+ }
2249
+ }
1971
2250
 
1972
2251
  @keyframes kpSheetUp {
1973
2252
  from { transform: translateY(100%); opacity: 0.8; }
1974
- to { transform: translateY(0); opacity: 1; }
2253
+ to { transform: translateY(0); opacity: 1; }
1975
2254
  }
1976
2255
 
1977
2256
  @keyframes kpSheetDown {
1978
- from { transform: translateY(0); opacity: 1; }
2257
+ from { transform: translateY(0); opacity: 1; }
1979
2258
  to { transform: translateY(100%); opacity: 0; }
1980
2259
  }
1981
2260
  `;
1982
2261
 
1983
2262
  function getStepTemplates(fiatAmount, currency) {
1984
2263
  return `
2264
+ <!-- Step 0: Initialising -->
1985
2265
  <div class="step active" id="kwespay-step0">
1986
2266
  <div class="kp-topbar">
1987
2267
  <div class="kp-topbar-brand">
1988
- <div class="kp-topbar-dot"></div>
1989
2268
  <span class="kp-topbar-name">KwesPay Checkout</span>
1990
2269
  </div>
1991
-
1992
2270
  </div>
1993
2271
  <div class="loading-container" style="flex:1">
1994
2272
  <div class="spinner-wrapper">
@@ -2007,10 +2285,10 @@ function getStepTemplates(fiatAmount, currency) {
2007
2285
  </div>
2008
2286
  </div>
2009
2287
 
2288
+ <!-- Step 0.5: API key invalid / network error on init -->
2010
2289
  <div class="step" id="kwespay-step0-invalid">
2011
2290
  <div class="kp-topbar">
2012
2291
  <div class="kp-topbar-brand">
2013
- <div class="kp-topbar-dot" style="background:var(--kp-red);box-shadow:none;animation:none"></div>
2014
2292
  <span class="kp-topbar-name">KwesPay Checkout</span>
2015
2293
  </div>
2016
2294
  </div>
@@ -2026,13 +2304,12 @@ function getStepTemplates(fiatAmount, currency) {
2026
2304
  </div>
2027
2305
  </div>
2028
2306
 
2307
+ <!-- Step 1: Select Network -->
2029
2308
  <div class="step" id="kwespay-step1">
2030
2309
  <div class="kp-topbar">
2031
2310
  <div class="kp-topbar-brand">
2032
- <div class="kp-topbar-dot"></div>
2033
2311
  <span class="kp-topbar-name">KwesPay Checkout</span>
2034
2312
  </div>
2035
-
2036
2313
  </div>
2037
2314
  <div class="kp-amount-block">
2038
2315
  <div class="kp-amount-label">Total due</div>
@@ -2060,21 +2337,19 @@ function getStepTemplates(fiatAmount, currency) {
2060
2337
  </div>
2061
2338
  </div>
2062
2339
 
2340
+ <!-- Step 2: Select Token -->
2063
2341
  <div class="step" id="kwespay-step2">
2064
2342
  <div class="kp-topbar">
2065
2343
  <div class="kp-topbar-brand">
2066
2344
  <button class="kp-back-btn" id="kwespay-back2">
2067
2345
  <span class="material-symbols-outlined">arrow_back</span>
2068
2346
  </button>
2069
- <div class="kp-topbar-dot"></div>
2070
2347
  <span class="kp-topbar-name">KwesPay Checkout</span>
2071
2348
  </div>
2072
-
2073
2349
  </div>
2074
2350
  <div class="kp-amount-block">
2075
2351
  <div class="kp-amount-label">Total due</div>
2076
2352
  <div class="kp-amount-value">${fiatAmount} ${currency}</div>
2077
- <div class="kp-amount-hint">Select a token — exact amount shown at review</div>
2078
2353
  </div>
2079
2354
  <div class="progress-section">
2080
2355
  <div class="progress-info">
@@ -2103,16 +2378,15 @@ function getStepTemplates(fiatAmount, currency) {
2103
2378
  </div>
2104
2379
  </div>
2105
2380
 
2381
+ <!-- Step 3: Review & Pay -->
2106
2382
  <div class="step" id="kwespay-step3">
2107
2383
  <div class="kp-topbar">
2108
2384
  <div class="kp-topbar-brand">
2109
2385
  <button class="kp-back-btn" id="kwespay-back3">
2110
2386
  <span class="material-symbols-outlined">arrow_back</span>
2111
2387
  </button>
2112
- <div class="kp-topbar-dot"></div>
2113
2388
  <span class="kp-topbar-name">KwesPay Checkout</span>
2114
2389
  </div>
2115
-
2116
2390
  </div>
2117
2391
  <div class="progress-section">
2118
2392
  <div class="progress-info">
@@ -2134,37 +2408,35 @@ function getStepTemplates(fiatAmount, currency) {
2134
2408
  <span id="kwespay-quoteTimerText">—</span>
2135
2409
  </div>
2136
2410
  </div>
2137
-
2138
2411
  <div class="kp-detail-block">
2139
2412
  <div class="kp-detail-row">
2140
- <span class="kp-detail-key">wallet</span>
2413
+ <span class="kp-detail-key">Wallet</span>
2141
2414
  <span class="kp-detail-val" id="kwespay-connectedWalletAddress">—</span>
2142
2415
  </div>
2143
2416
  <div class="kp-detail-row">
2144
- <span class="kp-detail-key">network</span>
2417
+ <span class="kp-detail-key">Network</span>
2145
2418
  <span class="kp-detail-val" id="kwespay-summaryNetwork">—</span>
2146
2419
  </div>
2147
2420
  <div class="kp-detail-row">
2148
- <span class="kp-detail-key">token</span>
2421
+ <span class="kp-detail-key">Token</span>
2149
2422
  <span class="kp-detail-val accent" id="kwespay-summaryToken">—</span>
2150
2423
  </div>
2151
2424
  </div>
2152
-
2153
2425
  <div class="kp-fee-block">
2154
2426
  <div class="kp-fee-header">
2155
2427
  <span class="material-symbols-outlined">receipt_long</span>
2156
2428
  <span class="kp-fee-header-text">Fee Breakdown</span>
2157
2429
  </div>
2158
2430
  <div class="kp-detail-row">
2159
- <span class="kp-detail-key">payment amount</span>
2431
+ <span class="kp-detail-key">Payment amount</span>
2160
2432
  <span class="kp-detail-val" id="kwespay-feePaymentAmount">—</span>
2161
2433
  </div>
2162
2434
  <div class="kp-detail-row">
2163
- <span class="kp-detail-key">platform fee (0.25%)</span>
2435
+ <span class="kp-detail-key">Platform fee (0.25%)</span>
2164
2436
  <span class="kp-detail-val" id="kwespay-feePlatformFee">—</span>
2165
2437
  </div>
2166
2438
  <div class="kp-detail-row">
2167
- <span class="kp-detail-key">vendor receives</span>
2439
+ <span class="kp-detail-key">Vendor receives</span>
2168
2440
  <span class="kp-detail-val green" id="kwespay-feeVendorAmount">—</span>
2169
2441
  </div>
2170
2442
  </div>
@@ -2175,14 +2447,18 @@ function getStepTemplates(fiatAmount, currency) {
2175
2447
  </div>
2176
2448
  </div>
2177
2449
 
2450
+ <!-- Step 4: Processing / Confirming / Success (mutating sub-views) -->
2178
2451
  <div class="step" id="kwespay-step4">
2179
2452
  <div class="kp-topbar">
2180
2453
  <div class="kp-topbar-brand">
2181
- <div class="kp-topbar-dot"></div>
2182
- <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>
2183
2456
  </div>
2457
+
2184
2458
  </div>
2185
- <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">
2186
2462
  <div class="spinner-wrapper">
2187
2463
  <div class="spinner-ring"></div>
2188
2464
  <div class="spinner-ring-2"></div>
@@ -2200,62 +2476,92 @@ function getStepTemplates(fiatAmount, currency) {
2200
2476
  </div>
2201
2477
  </div>
2202
2478
  </div>
2203
- </div>
2204
2479
 
2205
- <div class="step" id="kwespay-step5">
2206
- <div class="kp-topbar">
2207
- <div class="kp-topbar-brand">
2208
- <div class="kp-topbar-dot" style="background:var(--kp-green);box-shadow:0 0 8px rgba(16,185,129,0.4);animation:none"></div>
2209
- <span class="kp-topbar-name">Payment Complete</span>
2210
- </div>
2211
- <div class="kp-topbar-secure">
2212
- <span class="material-symbols-outlined" style="color:var(--kp-green)">check_circle</span>
2213
- 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>
2214
2488
  </div>
2489
+ <h2 class="headline">Confirming Payment</h2>
2490
+ <p class="body-text" id="kwespay-confirmingText">Waiting for network confirmation.</p>
2215
2491
  </div>
2216
- <div class="loading-container" style="flex:1">
2217
- <div class="success-icon">
2218
- <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>
2219
2500
  </div>
2220
- <h2 class="headline">Payment Successful</h2>
2221
- <p class="body-text">Your transaction has been confirmed on-chain.</p>
2222
- <div class="tx-details">
2223
- <div class="tx-row">
2224
- <span class="tx-label">tx hash</span>
2225
- <div class="tx-hash-row">
2226
- <span class="tx-value" id="kwespay-txHash">—</span>
2227
- <a class="explorer-link" id="kwespay-explorerLink" target="_blank" rel="noopener noreferrer">
2228
- <span class="material-symbols-outlined">open_in_new</span>
2229
- </a>
2230
- </div>
2231
- </div>
2232
- <div class="tx-row">
2233
- <span class="tx-label">amount paid</span>
2234
- <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>
2235
2506
  </div>
2236
- <div class="tx-row">
2237
- <span class="tx-label">crypto amount</span>
2238
- <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>
2239
2540
  </div>
2240
- <div class="tx-row">
2241
- <span class="tx-label">network</span>
2242
- <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>
2243
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>
2244
2556
  </div>
2245
- </div>
2246
- <div class="bottom-action">
2247
- <button class="action-btn" id="kwespay-closeSuccessBtn">Done</button>
2248
- </div>
2249
- <div class="kp-footer">
2250
- <span class="material-symbols-outlined kp-footer-lock" style="font-size:12px">lock</span>
2251
- <span class="kp-footer-text">Secured by <span>KwesPay</span> &middot; On-chain verified</span>
2252
2557
  </div>
2253
2558
  </div>
2254
2559
 
2255
- <div class="step" id="kwespay-step6">
2560
+ <!-- Step 5: Error / Failed -->
2561
+ <div class="step" id="kwespay-step5">
2256
2562
  <div class="kp-topbar">
2257
2563
  <div class="kp-topbar-brand">
2258
- <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>
2259
2565
  <span class="kp-topbar-name">Payment Failed</span>
2260
2566
  </div>
2261
2567
  </div>
@@ -2265,6 +2571,12 @@ function getStepTemplates(fiatAmount, currency) {
2265
2571
  </div>
2266
2572
  <h2 class="headline" id="kwespay-errorTitle">Something went wrong</h2>
2267
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>
2268
2580
  </div>
2269
2581
  <div class="bottom-action">
2270
2582
  <button class="action-btn" id="kwespay-retryPayment">Try Again</button>
@@ -2311,27 +2623,29 @@ const DomMethods = {
2311
2623
  overlay.className = "kwespay-overlay";
2312
2624
  overlay.id = "kwespay-widget-overlay";
2313
2625
 
2626
+ const container = document.createElement("div");
2627
+ container.className = "kwespay-container";
2628
+ container.id = "kwespay-widget-container";
2629
+
2314
2630
  const closeBtn = document.createElement("button");
2315
2631
  closeBtn.className = "kwespay-close-btn";
2316
2632
  closeBtn.innerHTML = "×";
2317
2633
  closeBtn.onclick = () => this.close();
2318
- overlay.appendChild(closeBtn);
2634
+ container.appendChild(closeBtn);
2319
2635
 
2320
- const container = document.createElement("div");
2321
- container.className = "kwespay-container";
2322
- container.id = "kwespay-widget-container";
2323
- container.innerHTML = getStepTemplates(
2324
- this.config.amount,
2636
+ const stepsWrapper = document.createElement("div");
2637
+ stepsWrapper.className = "kwespay-steps-wrapper";
2638
+ stepsWrapper.innerHTML = getStepTemplates(
2639
+ this._displayAmount,
2325
2640
  this.config.currency
2326
2641
  );
2642
+ container.appendChild(stepsWrapper);
2327
2643
 
2328
2644
  overlay.appendChild(container);
2329
2645
  document.body.appendChild(overlay);
2330
2646
 
2331
2647
  this._setupEventListeners();
2332
2648
  this._setupSwipeToClose(container);
2333
-
2334
- // Clicking outside the container does NOT close the widget (intentional)
2335
2649
  },
2336
2650
 
2337
2651
  _setupSwipeToClose(container) {
@@ -2502,8 +2816,11 @@ const NavMethods = {
2502
2816
  targetStep = document.getElementById(`kwespay-step-${stepNumber}`);
2503
2817
  else targetStep = document.getElementById(`kwespay-step${stepNumber}`);
2504
2818
 
2505
- if (targetStep) targetStep.classList.add("active");
2506
- 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
+ }
2507
2824
  },
2508
2825
 
2509
2826
  _removeCustomStep(id) {
@@ -2513,13 +2830,16 @@ const NavMethods = {
2513
2830
  _showError(title, message) {
2514
2831
  const titleEl = document.getElementById("kwespay-errorTitle");
2515
2832
  const msgEl = document.getElementById("kwespay-errorMessage");
2516
- if (titleEl) titleEl.textContent = title;
2517
- if (msgEl) msgEl.textContent = message;
2518
- 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);
2519
2838
  },
2520
2839
 
2521
2840
  _reset() {
2522
2841
  this._clearQuoteTimer();
2842
+ this._stopReceiptCountdown();
2523
2843
  this._removeCustomStep("kwespay-step-wallet-picker");
2524
2844
  this._removeCustomStep("kwespay-step-wc");
2525
2845
  this.state.selectedNetwork = null;
@@ -2587,23 +2907,13 @@ const NetworkMethods = {
2587
2907
  const listItem = document.createElement("div");
2588
2908
  listItem.className = "list-item";
2589
2909
  listItem.innerHTML = `
2590
- <div class="item-icon"><img src="${network.logo}" alt="${
2591
- network.name
2592
- }" /></div>
2910
+ <div class="item-icon"><img src="${network.logo}" alt="${network.name}" /></div>
2593
2911
  <div class="item-info">
2594
2912
  <div class="item-name-row">
2595
2913
  <p class="item-name">${network.name}</p>
2596
- ${
2597
- network.type === "testnet"
2598
- ? '<span class="item-badge badge-testnet">Testnet</span>'
2599
- : ""
2600
- }
2914
+
2601
2915
  </div>
2602
- <p class="item-desc">${
2603
- network.type === "mainnet"
2604
- ? "Mainnet · Production"
2605
- : "Testnet · Development"
2606
- }</p>
2916
+
2607
2917
  </div>
2608
2918
  <span class="material-symbols-outlined item-chevron">chevron_right</span>
2609
2919
  `;
@@ -2742,7 +3052,7 @@ const WalletMethods = {
2742
3052
  <button class="kp-back-btn" id="kwespay-picker-back">
2743
3053
  <span class="material-symbols-outlined">arrow_back</span>
2744
3054
  </button>
2745
- <div class="kp-topbar-dot"></div>
3055
+
2746
3056
  <span class="kp-topbar-name">Connect Wallet</span>
2747
3057
  </div>
2748
3058
  </div>
@@ -2912,7 +3222,7 @@ const WalletMethods = {
2912
3222
  <button class="kp-back-btn" id="kwespay-wc-back">
2913
3223
  <span class="material-symbols-outlined">arrow_back</span>
2914
3224
  </button>
2915
- <div class="kp-topbar-dot"></div>
3225
+
2916
3226
  <span class="kp-topbar-name">Scan QR Code</span>
2917
3227
  </div>
2918
3228
  </div>
@@ -2952,7 +3262,7 @@ const WalletMethods = {
2952
3262
  <button class="kp-back-btn" id="kwespay-wc-back">
2953
3263
  <span class="material-symbols-outlined">arrow_back</span>
2954
3264
  </button>
2955
- <div class="kp-topbar-dot"></div>
3265
+
2956
3266
  <span class="kp-topbar-name">Connect Wallet</span>
2957
3267
  </div>
2958
3268
  </div>
@@ -3102,8 +3412,6 @@ const WalletMethods = {
3102
3412
  },
3103
3413
  };
3104
3414
 
3105
- const PLATFORM_FEE_BPS = 25;
3106
-
3107
3415
  function formatUnits(rawBigInt, decimals) {
3108
3416
  const divisor = BigInt(10 ** decimals);
3109
3417
  const whole = rawBigInt / divisor;
@@ -3131,6 +3439,31 @@ function formatUnits(rawBigInt, decimals) {
3131
3439
  return `0.${fracFull.slice(0, firstSig) + sigSlice}`;
3132
3440
  }
3133
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
+
3134
3467
  const QuoteMethods = {
3135
3468
  async _loadReviewStep() {
3136
3469
  this._clearQuoteTimer();
@@ -3143,47 +3476,65 @@ const QuoteMethods = {
3143
3476
  const proceedBtn = document.getElementById("kwespay-proceedToPayment");
3144
3477
 
3145
3478
  if (cryptoLine) {
3146
- cryptoLine.textContent = "loading";
3479
+ cryptoLine.textContent = "loading...";
3147
3480
  cryptoLine.classList.add("loading");
3148
3481
  }
3149
3482
  if (timerEl) timerEl.style.display = "none";
3150
3483
  if (proceedBtn) proceedBtn.disabled = true;
3151
3484
 
3152
3485
  try {
3153
- const payload = await this.paymentService.getQuote({
3154
- vendorId: this.config.vendorId,
3486
+ const quote = await this.paymentService.getQuote({
3487
+ vendorIdentifier: this.config.vendorId,
3155
3488
  cryptoCurrency: this.state.selectedToken,
3156
3489
  fiatAmount: this.config.amount,
3157
3490
  fiatCurrency: this.config.currency,
3158
3491
  network: this.state.selectedNetwork,
3159
- payerWalletAddress: this.walletService.getAddress(),
3160
3492
  });
3161
3493
 
3162
- this.state.currentPayload = payload;
3163
-
3164
- const decimals = this.state.selectedTokenConfig?.decimals ?? 6;
3165
- const amountBig = BigInt(payload.amountBaseUnits);
3166
- const feeNum = (Number(amountBig) * PLATFORM_FEE_BPS) / 10000;
3167
- const feeBig = BigInt(Math.max(1, Math.round(feeNum)));
3168
- const vendorBig = amountBig - feeBig;
3494
+ const decimals = this.state.selectedTokenConfig?.decimals ?? 18;
3169
3495
  const sym = this.state.selectedToken;
3170
- 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;
3171
3523
 
3172
3524
  if (cryptoLine) {
3173
- cryptoLine.textContent = fmt(amountBig);
3525
+ cryptoLine.textContent = fmt(totalBig);
3174
3526
  cryptoLine.classList.remove("loading");
3175
3527
  }
3176
- if (feeAmount) feeAmount.textContent = fmt(amountBig);
3528
+ if (feeAmount) feeAmount.textContent = fmt(totalBig);
3177
3529
  if (feePlatform) feePlatform.textContent = fmt(feeBig);
3178
- if (feeVendor)
3179
- feeVendor.textContent = fmt(vendorBig < 0n ? 0n : vendorBig);
3530
+ if (feeVendor) feeVendor.textContent = fmt(amountBig);
3180
3531
  if (proceedBtn) proceedBtn.disabled = false;
3181
3532
 
3182
- this._startQuoteTimer(payload.expiresAt);
3533
+ this._startQuoteTimer(quote.expiresAt);
3183
3534
  } catch (err) {
3184
- console.error("[KwesPayWidget] Quote fetch failed:", err.message);
3535
+ console.error("[KwesPay] Quote fetch failed:", err.message);
3185
3536
  if (cryptoLine) {
3186
- cryptoLine.textContent = "Could not load rate — please try again.";
3537
+ cryptoLine.textContent = "Could not load please try again.";
3187
3538
  cryptoLine.classList.remove("loading");
3188
3539
  }
3189
3540
  if (proceedBtn) proceedBtn.disabled = true;
@@ -3201,7 +3552,8 @@ const QuoteMethods = {
3201
3552
  const secs = Math.ceil(remaining / 1000);
3202
3553
  const mins = Math.floor(secs / 60);
3203
3554
  const s = secs % 60;
3204
- timerText.textContent = `Price locks in ${mins}:${String(s).padStart(
3555
+
3556
+ timerText.textContent = `Quote expires in ${mins}:${String(s).padStart(
3205
3557
  2,
3206
3558
  "0"
3207
3559
  )}`;
@@ -3211,7 +3563,7 @@ const QuoteMethods = {
3211
3563
  (secs <= 0 ? " expired" : "");
3212
3564
 
3213
3565
  if (secs <= 0) {
3214
- timerText.textContent = "Refreshing your rate";
3566
+ timerText.textContent = "Refreshing your rate...";
3215
3567
  const proceedBtn = document.getElementById("kwespay-proceedToPayment");
3216
3568
  if (proceedBtn) proceedBtn.disabled = true;
3217
3569
  this._clearQuoteTimer();
@@ -3231,6 +3583,8 @@ const QuoteMethods = {
3231
3583
  },
3232
3584
  };
3233
3585
 
3586
+ const RECEIPT_DWELL_MS = 10_000;
3587
+
3234
3588
  const PaymentMethods = {
3235
3589
  async _handlePaymentProcessing() {
3236
3590
  if (!this.state.currentPayload) {
@@ -3244,6 +3598,7 @@ const PaymentMethods = {
3244
3598
  try {
3245
3599
  this._clearQuoteTimer();
3246
3600
  this._goToStep(4);
3601
+ this._setProcessingView("processing");
3247
3602
 
3248
3603
  const setStatus = (title, text) => {
3249
3604
  const titleEl = document.getElementById("kwespay-processingTitle");
@@ -3254,27 +3609,34 @@ const PaymentMethods = {
3254
3609
 
3255
3610
  const isWC = this.walletService.connectionType === "walletconnect";
3256
3611
  const isMobile = this.walletService.isMobile();
3257
- const strictMobile = isWC && isMobile; // mobile WC path — no network switching, no RPC shortcuts
3612
+ const strictMobile = isWC && isMobile;
3258
3613
  const provider = this.walletService.getProvider();
3259
3614
  const targetChainId = this.state.selectedChainId;
3260
3615
 
3261
- if (!provider) throw new Error("No wallet provider");
3262
-
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
+ }
3263
3626
 
3264
- const alive = await this.walletService.isSessionAlive();
3627
+ const alive = await this.walletService
3628
+ .isSessionAlive()
3629
+ .catch(() => false);
3265
3630
  if (!alive) {
3266
- console.error(
3267
- "[KwesPay] Session liveness check failed — session appears stale"
3268
- );
3269
- await this.walletService.disconnect();
3270
- const err = new Error(
3271
- "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" }
3272
3637
  );
3273
- err.code = "SESSION_EXPIRED";
3274
- throw err;
3275
3638
  }
3276
3639
 
3277
-
3278
3640
  if (isMobile) {
3279
3641
  document
3280
3642
  .getElementById("kwespay-mobileTransactionInstruction")
@@ -3282,17 +3644,20 @@ const PaymentMethods = {
3282
3644
  }
3283
3645
 
3284
3646
  if (strictMobile) {
3285
-
3286
3647
  await this._assertMobileChain(provider, targetChainId);
3287
3648
  } else {
3288
- 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
+ }
3289
3660
  const currentChainId = parseInt(rawChain, 16);
3290
-
3291
- console.log("[KwesPay] Desktop chain check —", {
3292
- currentChainId,
3293
- targetChainId,
3294
- });
3295
-
3296
3661
  if (currentChainId !== targetChainId) {
3297
3662
  setStatus(
3298
3663
  "Switching network…",
@@ -3305,11 +3670,9 @@ const PaymentMethods = {
3305
3670
  this.state.selectedToken,
3306
3671
  this.state.selectedTokenConfig.decimals
3307
3672
  );
3308
- console.log("[KwesPay] Network switched");
3309
3673
  }
3310
3674
  }
3311
3675
 
3312
-
3313
3676
  if (strictMobile) {
3314
3677
  setStatus(
3315
3678
  "Opening your wallet…",
@@ -3324,56 +3687,111 @@ const PaymentMethods = {
3324
3687
  );
3325
3688
  }
3326
3689
 
3327
- const receipt = await this.paymentService.createPayment({
3328
- payload: this.state.currentPayload,
3329
- walletProvider: provider,
3330
- onStatusUpdate: setStatus,
3331
- });
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
+ }
3332
3714
 
3333
-
3334
3715
  document
3335
3716
  .getElementById("kwespay-mobileTransactionInstruction")
3336
3717
  ?.style.setProperty("display", "none");
3337
3718
 
3338
-
3339
- const decimals = this.state.selectedTokenConfig?.decimals ?? 6;
3340
- const amountBig = BigInt(this.state.currentPayload.amountBaseUnits);
3341
- const cryptoDisplay = `${formatUnits(amountBig, decimals)} ${
3342
- this.state.selectedToken
3343
- }`;
3719
+ this._setProcessingView("confirming");
3344
3720
 
3345
- document.getElementById("kwespay-txHash").textContent = truncateHash(
3346
- receipt.hash
3347
- );
3348
- document.getElementById(
3349
- "kwespay-txFiatAmount"
3350
- ).textContent = `${this.config.amount} ${this.config.currency}`;
3351
- document.getElementById("kwespay-txCryptoAmount").textContent =
3352
- cryptoDisplay;
3353
- document.getElementById("kwespay-txNetwork").textContent =
3354
- this.state.selectedNetworkName;
3355
- document.getElementById("kwespay-explorerLink").href =
3356
- NETWORK_CONFIGS[this.state.selectedNetwork].explorer + receipt.hash;
3357
-
3358
- this._goToStep(5);
3359
-
3360
- dispatchWidgetEvent("paymentSuccess", {
3721
+ const onChainPayload = {
3361
3722
  transactionReference: receipt.transactionReference,
3362
3723
  paymentIdBytes32: receipt.paymentIdBytes32,
3363
3724
  transactionHash: receipt.hash,
3725
+ transactionStatus: "pending",
3364
3726
  fiatAmount: this.config.amount,
3365
3727
  currency: this.config.currency,
3366
3728
  token: this.state.selectedToken,
3367
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",
3368
3749
  });
3369
- } catch (error) {
3370
- console.error(
3371
- "[KwesPay] Payment error —",
3372
- error.code ?? "UNKNOWN",
3373
- ":",
3374
- 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}`
3375
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.txHash;
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
+ };
3376
3792
 
3793
+ this._finalisePayment(finalPayload, false);
3794
+ } catch (error) {
3377
3795
  document
3378
3796
  .getElementById("kwespay-mobileTransactionInstruction")
3379
3797
  ?.style.setProperty("display", "none");
@@ -3382,71 +3800,171 @@ const PaymentMethods = {
3382
3800
  let title = "Payment Failed";
3383
3801
  let message = getErrorMessage(error, { token: this.state.selectedToken });
3384
3802
 
3385
- if (error.code === "SESSION_EXPIRED") {
3386
- title = "Session Expired";
3387
- message = error.message;
3388
- } else if (error.code === "WRONG_NETWORK") {
3389
- title = "Wrong Network";
3390
- message = error.message;
3391
- } else if (errorType === "USER_REJECTED") {
3392
- title = "Transaction Cancelled";
3393
- message = "You rejected the transaction in your wallet.";
3394
- } else if (errorType === "INSUFFICIENT_BALANCE") {
3395
- 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
+ }
3396
3834
  }
3397
3835
 
3836
+ console.error("[KwesPayWidget] Payment error:", {
3837
+ title,
3838
+ message,
3839
+ code: error.code,
3840
+ error,
3841
+ });
3842
+
3398
3843
  this._showError(title, message);
3399
- dispatchWidgetEvent("paymentError", { error: message, errorType });
3844
+ this._failPayment(message, errorType);
3400
3845
  }
3401
3846
  },
3402
3847
 
3403
- /**
3404
- * Confirm the wallet is on the target chain before sending a transaction.
3405
- * Retries up to 3 times with 1s delay to handle WC relay propagation lag.
3406
- * Throws WRONG_NETWORK if the chain never matches.
3407
- *
3408
- * MOBILE WC ONLY — never call this on desktop/injected.
3409
- */
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
+
3410
3944
  async _assertMobileChain(provider, targetChainId) {
3411
3945
  const MAX_ATTEMPTS = 3;
3412
3946
  const DELAY_MS = 1000;
3413
3947
 
3414
3948
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
3415
3949
  let currentChainId = null;
3416
-
3417
3950
  try {
3418
3951
  const raw = await provider.request({ method: "eth_chainId" });
3419
3952
  currentChainId = parseInt(raw, 16);
3420
- } catch (err) {
3421
- console.error(
3422
- `[KwesPay] eth_chainId RPC failed (attempt ${attempt}/${MAX_ATTEMPTS}):`,
3423
- err.message
3424
- );
3425
- }
3426
-
3427
- console.log(
3428
- `[KwesPay] Chain check attempt ${attempt}/${MAX_ATTEMPTS} —`,
3429
- { currentChainId, targetChainId }
3430
- );
3431
-
3432
- if (currentChainId === targetChainId) {
3433
- console.log("[KwesPay] Chain confirmed ✅", currentChainId);
3434
- return;
3435
- }
3953
+ } catch {}
3436
3954
 
3437
- if (attempt < MAX_ATTEMPTS) {
3955
+ if (currentChainId === targetChainId) return;
3956
+ if (attempt < MAX_ATTEMPTS)
3438
3957
  await new Promise((r) => setTimeout(r, DELAY_MS));
3439
- }
3440
3958
  }
3441
3959
 
3442
- const err = new Error(
3443
- `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" }
3444
3965
  );
3445
- err.code = "WRONG_NETWORK";
3446
- throw err;
3447
3966
  },
3448
3967
 
3449
-
3450
3968
  async _switchNetworkSafe(
3451
3969
  chainId,
3452
3970
  networkName,
@@ -3454,9 +3972,7 @@ const PaymentMethods = {
3454
3972
  tokenSymbol,
3455
3973
  tokenDecimals
3456
3974
  ) {
3457
- const switchNetwork = this.walletService.switchNetwork;
3458
3975
  const provider = this.walletService.getProvider();
3459
-
3460
3976
  if (!provider) throw new Error("[WalletService] No provider connected");
3461
3977
 
3462
3978
  const toHex = (val) => {
@@ -3481,15 +3997,10 @@ const PaymentMethods = {
3481
3997
  await provider.request({ method: "eth_chainId" })
3482
3998
  );
3483
3999
  if (currentHex && currentHex === targetHex) return;
3484
- } catch (err) {
3485
- console.warn(
3486
- "[KwesPay] Could not read chainId before switch:",
3487
- err.message
3488
- );
3489
- }
4000
+ } catch {}
3490
4001
 
3491
4002
  try {
3492
- await switchNetwork(
4003
+ await this.walletService.switchNetwork(
3493
4004
  chainId,
3494
4005
  networkName,
3495
4006
  rpcUrl,
@@ -3497,15 +4008,9 @@ const PaymentMethods = {
3497
4008
  tokenDecimals
3498
4009
  );
3499
4010
  } catch (err) {
3500
- if (err.code === 4001) throw err; // user rejected
3501
- // Some wallets throw even on success — continue to verify below
3502
- console.warn(
3503
- "[KwesPay] switchNetwork threw (verifying anyway):",
3504
- err.message
3505
- );
4011
+ if (err.code === 4001) throw err;
3506
4012
  }
3507
4013
 
3508
- // Poll until confirmed or 15s timeout
3509
4014
  const POLL_MS = 500;
3510
4015
  const TIMEOUT_MS = 15_000;
3511
4016
  const started = Date.now();
@@ -3516,17 +4021,8 @@ const PaymentMethods = {
3516
4021
  const currentHex = toHex(
3517
4022
  await provider.request({ method: "eth_chainId" })
3518
4023
  );
3519
- if (currentHex && currentHex === targetHex) {
3520
- console.log(
3521
- `[KwesPay] Network switch confirmed after ${
3522
- Date.now() - started
3523
- }ms ✅`
3524
- );
3525
- return;
3526
- }
3527
- } catch (err) {
3528
- console.warn("[KwesPay] Poll eth_chainId error:", err.message);
3529
- }
4024
+ if (currentHex && currentHex === targetHex) return;
4025
+ } catch {}
3530
4026
  }
3531
4027
 
3532
4028
  throw new Error(
@@ -3544,6 +4040,14 @@ function resolveAcceptedTokens(input) {
3544
4040
  return null;
3545
4041
  }
3546
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
+
3547
4051
  class KwesPayWidget {
3548
4052
  constructor(config) {
3549
4053
  if (!config.apiKey) throw new Error("[KwesPayWidget] apiKey is required");
@@ -3559,6 +4063,10 @@ class KwesPayWidget {
3559
4063
  currency: config.currency || DEFAULT_CONFIG.currency,
3560
4064
  graphqlEndpoint: DEFAULT_CONFIG.graphqlEndpoint,
3561
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,
3562
4070
  };
3563
4071
 
3564
4072
  if (!Object.values(SUPPORTED_CURRENCIES).includes(this.config.currency)) {
@@ -3591,21 +4099,61 @@ class KwesPayWidget {
3591
4099
  wcUri: null,
3592
4100
  };
3593
4101
 
4102
+ this._paymentResolve = null;
4103
+ this._paymentReject = null;
4104
+ this._finalised = false;
4105
+ this._receiptCountdownInterval = null;
4106
+
3594
4107
  this._init();
3595
4108
  }
3596
4109
 
3597
- 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
+
3598
4121
  const overlay = document.getElementById("kwespay-widget-overlay");
3599
4122
  const container = document.getElementById("kwespay-widget-container");
3600
- if (!overlay || !container) return;
3601
-
3602
- container.classList.remove("closing");
3603
- overlay.classList.add("open");
3604
- document.body.classList.add("kwespay-open");
3605
- 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
+ }
3606
4129
 
3607
- await this._validateAPIKey();
4130
+ this._validateAPIKey();
3608
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);
3609
4157
  }
3610
4158
 
3611
4159
  close() {
@@ -3613,28 +4161,29 @@ class KwesPayWidget {
3613
4161
  const container = document.getElementById("kwespay-widget-container");
3614
4162
  if (!overlay || !container) return;
3615
4163
 
4164
+ this._stopReceiptCountdown();
3616
4165
  this._clearQuoteTimer();
3617
- const closedAfterSuccess = this.state.currentStep === 5;
4166
+
4167
+ const closedAfterSuccess = this.state.currentStep === 4;
3618
4168
  const mobile = window.innerWidth <= 480;
3619
4169
 
3620
- if (mobile) {
3621
- container.classList.add("closing");
3622
- setTimeout(() => {
3623
- container.classList.remove("closing");
3624
- overlay.classList.remove("open");
3625
- document.body.classList.remove("kwespay-open");
3626
- this.state.isOpen = false;
3627
- dispatchWidgetEvent("widgetClosed", {
3628
- completedPayment: closedAfterSuccess,
3629
- });
3630
- }, 300);
3631
- } else {
4170
+ const finish = () => {
3632
4171
  overlay.classList.remove("open");
3633
4172
  document.body.classList.remove("kwespay-open");
3634
4173
  this.state.isOpen = false;
3635
4174
  dispatchWidgetEvent("widgetClosed", {
3636
4175
  completedPayment: closedAfterSuccess,
3637
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();
3638
4187
  }
3639
4188
  }
3640
4189
 
@@ -3652,12 +4201,13 @@ class KwesPayWidget {
3652
4201
  ) {
3653
4202
  this.config.currency = newCurrency;
3654
4203
  }
4204
+ const display = `${this._displayAmount} ${this.config.currency}`;
3655
4205
  document
3656
4206
  .querySelectorAll(
3657
4207
  '[id*="paymentAmount"], [id*="summaryFiatAmount"], [id*="txFiatAmount"], [id*="reviewFiatAmount"]'
3658
4208
  )
3659
4209
  .forEach((el) => {
3660
- el.textContent = `${this.config.amount} ${this.config.currency}`;
4210
+ el.textContent = display;
3661
4211
  });
3662
4212
  dispatchWidgetEvent("amountUpdated", {
3663
4213
  amount: this.config.amount,
@@ -3670,11 +4220,21 @@ class KwesPayWidget {
3670
4220
  }
3671
4221
 
3672
4222
  destroy() {
4223
+ this._stopReceiptCountdown();
3673
4224
  this._clearQuoteTimer();
3674
4225
  document.body.classList.remove("kwespay-open");
3675
4226
  this.walletService?.disconnect();
3676
4227
  document.getElementById("kwespay-widget-overlay")?.remove();
3677
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
+
3678
4238
  this.state = null;
3679
4239
  this.config = null;
3680
4240
  this.walletService = null;
@@ -3694,9 +4254,103 @@ Object.assign(
3694
4254
  PaymentMethods
3695
4255
  );
3696
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
+
3697
4344
  /**
3698
- * KwesPay Widget - Main entry point
3699
- * @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").
3700
4354
  */
3701
4355
 
3702
- export { KwesPayWidget as default };
4356
+ export { KwesPayWidget, KwesPayWidget as default, kwespay };