QR and payment fixes

This commit is contained in:
Savya Bikram Shah
2026-05-07 16:43:28 +05:45
parent 906ebbcac9
commit 846a4fda9c
20 changed files with 135 additions and 433 deletions

View File

@@ -27,7 +27,7 @@ namespace Darkmatter.Fonepay.Samples
var fonepay = new FonepayClient();
var request = new QrRequest
{
amount = 1,
amount = 0.5f,
remarks1 = "mausham ko paisa"
};
var qr = await fonepay.PurchaseAsync(request, destroyCancellationToken);
@@ -45,7 +45,7 @@ namespace Darkmatter.Fonepay.Samples
qr.thirdpartyQrWebSocketUrl,
onQrVerified: v => Debug.Log($"Fonepay QR verified: {v}"),
ct: destroyCancellationToken);
Debug.Log($"{JsonUtility.ToJson(payment)}");
var ok = payment.Outcome == PaymentOutcome.Complete;
qrImage.gameObject.SetActive(false);

View File

@@ -177,14 +177,29 @@ namespace Darkmatter.Fonepay
return new QrResult
{
message = response.message,
qrCode = string.IsNullOrEmpty(response.qrMessage)
? null
: FonepayQRGenerator.GenerateTexture(response.qrMessage),
qrCode = TryGenerateQr(response.qrMessage),
status = response.status,
statusCode = response.statusCode,
success = response.success,
thirdpartyQrWebSocketUrl = response.thirdpartyQrWebSocketUrl,
qrMessage = response.qrMessage,
};
}
private static Texture2D TryGenerateQr(string qrMessage)
{
if (string.IsNullOrEmpty(qrMessage))
return null;
try
{
return FonepayQRGenerator.GenerateTexture(qrMessage);
}
catch (Exception ex)
{
Debug.LogWarning($"Fonepay QR render failed ({ex.Message}). " +
"Use QrResult.qrMessage with an external renderer.");
return null;
}
}
}
}

View File

@@ -13,6 +13,7 @@ namespace Darkmatter.Fonepay
internal event Action<bool> OnQrVerified;
internal event Action<WebsocketMessage<QRPaymentStatus>> OnPaymentReceived;
internal event Action<string> OnRawMessage;
internal event Action<Exception> OnClosed;
private ClientWebSocket _client;
private CancellationTokenSource _cts;
@@ -51,6 +52,7 @@ namespace Darkmatter.Fonepay
CancellationToken cancellationToken)
{
var buffer = new byte[4096];
Exception error = null;
try
{
@@ -70,9 +72,9 @@ namespace Darkmatter.Fonepay
{
// Expected during disconnect.
}
catch (WebSocketException)
catch (WebSocketException ex)
{
// Network disconnect or broken socket.
error = ex;
}
catch (ObjectDisposedException)
{
@@ -80,7 +82,11 @@ namespace Darkmatter.Fonepay
}
catch (Exception ex)
{
Console.WriteLine($"WebSocket receive error: {ex.Message}");
error = ex;
}
finally
{
OnClosed?.Invoke(error);
}
}

View File

@@ -12,6 +12,7 @@ namespace Darkmatter.Fonepay
public int statusCode;
public bool success;
public string thirdpartyQrWebSocketUrl;
public string qrMessage;
}

View File

@@ -7,7 +7,7 @@ namespace Darkmatter.Fonepay
{
public string remarks1;
public string remarks2;
public DateTime transactionDate;
public string transactionDate;
public string productNumber;
public float amount;
public string message;

View File

@@ -9,7 +9,6 @@ namespace Darkmatter.Fonepay
public string merchantId;
public string deviceId;
public string transactionStatus;
private T _status;
public T Status => _status ??= JsonUtility.FromJson<T>(transactionStatus);
public T Status => JsonUtility.FromJson<T>(transactionStatus);
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: debdfefd8b01242e8b95f0e29bb09c0d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013-2025 Raffael Herrmann
Copyright (c) 2024-2025 Shane Krueger
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3902a28335b7043bf831aab193145f4c
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: ecb337d209ad74a598de572b4e034307
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,40 +0,0 @@
using System;
namespace Darkmatter.Fonepay
{
public static partial class FonepayQRGenerator
{
// [version-1][eccLevel] = (totalCodewords, ecCodewordsPerBlock, blocks)
static readonly (int total, int ecPerBlock, int blocks)[,] _caps =
{
{ (19, 7, 1), (16, 10, 1), (13, 13, 1), (9, 17, 1) },
{ (34, 10, 1), (28, 16, 1), (22, 22, 1), (16, 28, 1) },
{ (55, 15, 1), (44, 26, 1), (34, 18, 2), (26, 22, 2) },
{ (80, 20, 2), (64, 18, 2), (48, 26, 4), (36, 16, 4) },
{ (108, 26, 2), (86, 24, 2), (62, 18, 2), (46, 22, 2) },
{ (136, 18, 4), (108, 16, 4), (76, 24, 4), (60, 28, 4) },
{ (156, 20, 4), (124, 18, 4), (88, 18, 6), (66, 26, 4) },
{ (194, 24, 4), (154, 22, 4), (110, 22, 6), (86, 26, 4) },
{ (232, 30, 4), (182, 22, 5), (132, 20, 8), (100, 24, 4) },
{ (274, 18, 6), (216, 26, 6), (154, 24, 8), (122, 28, 6) },
};
static int DataCodewords(int ver, EccLevel ecc)
{
var (total, ecPer, blks) = _caps[ver - 1, (int)ecc];
return total - ecPer * blks;
}
static (int version, (int total, int ecPerBlock, int blocks) ecBlocks)
ChooseVersion(int byteLen, EccLevel ecc)
{
for (int v = 1; v <= 10; v++)
{
int dc = DataCodewords(v, ecc);
if (dc >= byteLen + 2)
return (v, _caps[v - 1, (int)ecc]);
}
throw new Exception($"Data too large for versions 1-10 (byte mode, ecc={ecc})");
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: a611e576dae8b4ff6b15c314c1246cf1

View File

@@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
namespace Darkmatter.Fonepay
{
public static partial class FonepayQRGenerator
{
static byte[] BuildCodewords(byte[] data, int version, EccLevel ecc,
(int total, int ecPerBlock, int blocks) ecBlocks)
{
var bits = new List<bool>();
bits.AddRange(new[] { false, true, false, false }); // byte mode
int len = data.Length;
for (int i = 7; i >= 0; i--) bits.Add(((len >> i) & 1) == 1);
foreach (byte b in data)
for (int i = 7; i >= 0; i--)
bits.Add(((b >> i) & 1) == 1);
int dcBytes = DataCodewords(version, ecc);
int targetBits = dcBytes * 8;
for (int i = 0; i < 4 && bits.Count < targetBits; i++) bits.Add(false);
while (bits.Count % 8 != 0) bits.Add(false);
bool[] padA = { true, true, true, false, true, true, false, false };
bool[] padB = { false, false, false, true, false, false, false, true };
int pi = 0;
while (bits.Count < targetBits)
{
bits.AddRange(pi % 2 == 0 ? padA : padB);
pi++;
}
byte[] dc = new byte[dcBytes];
for (int i = 0; i < dcBytes; i++)
for (int b = 0; b < 8; b++)
if (bits[i * 8 + b])
dc[i] |= (byte)(1 << (7 - b));
int blocks = ecBlocks.blocks;
int ecPer = ecBlocks.ecPerBlock;
int blockSize = dcBytes / blocks;
byte[][] dcBlocks = new byte[blocks][];
byte[][] ecBlocksArr = new byte[blocks][];
for (int i = 0; i < blocks; i++)
{
dcBlocks[i] = new byte[blockSize];
Array.Copy(dc, i * blockSize, dcBlocks[i], 0, blockSize);
ecBlocksArr[i] = ReedSolomon(dcBlocks[i], ecPer);
}
var result = new List<byte>();
for (int i = 0; i < blockSize; i++)
for (int b = 0; b < blocks; b++)
result.Add(dcBlocks[b][i]);
for (int i = 0; i < ecPer; i++)
for (int b = 0; b < blocks; b++)
result.Add(ecBlocksArr[b][i]);
return result.ToArray();
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: e67574ccbe68944928d706bd99d785ca

View File

@@ -1,234 +0,0 @@
using System;
namespace Darkmatter.Fonepay
{
public static partial class FonepayQRGenerator
{
static bool[,] BuildMatrix(int version, byte[] codewords)
{
int size = version * 4 + 17;
var matrix = new bool[size, size];
var reserved = new bool[size, size];
PlaceFinder(matrix, reserved, 0, 0);
PlaceFinder(matrix, reserved, 0, size - 7);
PlaceFinder(matrix, reserved, size - 7, 0);
PlaceTiming(matrix, reserved, size);
PlaceDarkModule(matrix, reserved, version);
if (version >= 2) PlaceAlignment(matrix, reserved, version);
ReserveFormat(reserved, size);
PlaceData(matrix, reserved, codewords, size);
int bestPenalty = int.MaxValue;
bool[,] bestMatrix = null;
for (int m = 0; m < 8; m++)
{
var candidate = (bool[,])matrix.Clone();
ApplyMask(candidate, reserved, m, size);
ApplyFormatInfo(candidate, reserved, 1, m, size);
int penalty = CalcPenalty(candidate, size);
if (penalty < bestPenalty)
{
bestPenalty = penalty;
bestMatrix = (bool[,])candidate.Clone();
}
}
return bestMatrix;
}
static void PlaceFinder(bool[,] m, bool[,] r, int row, int col)
{
for (int dr = -1; dr <= 7; dr++)
for (int dc = -1; dc <= 7; dc++)
{
int rr = row + dr, cc = col + dc;
if (rr < 0 || cc < 0 || rr >= m.GetLength(0) || cc >= m.GetLength(1)) continue;
r[rr, cc] = true;
bool inOuter = dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6;
bool inBorder = (dr == 0 || dr == 6 || dc == 0 || dc == 6) && inOuter;
bool inInner = dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4;
m[rr, cc] = !(dr == -1 || dc == -1 || dr == 7 || dc == 7) && (inBorder || inInner);
}
}
static void PlaceTiming(bool[,] m, bool[,] r, int size)
{
for (int i = 8; i < size - 8; i++)
{
bool v = i % 2 == 0;
m[6, i] = v; r[6, i] = true;
m[i, 6] = v; r[i, 6] = true;
}
}
static void PlaceDarkModule(bool[,] m, bool[,] r, int ver)
{
int row = ver * 4 + 9;
m[row, 8] = true;
r[row, 8] = true;
}
static readonly int[][] _alignCenters =
{
new int[] { },
new[] { 6, 18 },
new[] { 6, 22 },
new[] { 6, 26 },
new[] { 6, 30 },
new[] { 6, 34 },
new[] { 6, 22, 38 },
new[] { 6, 24, 42 },
new[] { 6, 26, 46 },
new[] { 6, 28, 50 },
};
static void PlaceAlignment(bool[,] m, bool[,] r, int version)
{
int[] centers = _alignCenters[version - 1];
foreach (int row in centers)
foreach (int col in centers)
{
if (r[row, col]) continue;
for (int dr = -2; dr <= 2; dr++)
for (int dc = -2; dc <= 2; dc++)
{
bool dark = Math.Abs(dr) == 2 || Math.Abs(dc) == 2 || (dr == 0 && dc == 0);
m[row + dr, col + dc] = dark;
r[row + dr, col + dc] = true;
}
}
}
static void ReserveFormat(bool[,] r, int size)
{
for (int i = 0; i <= 8; i++)
{
r[8, i] = true;
r[i, 8] = true;
}
for (int i = size - 8; i < size; i++)
{
r[8, i] = true;
r[i, 8] = true;
}
}
static void PlaceData(bool[,] m, bool[,] r, byte[] cw, int size)
{
int cwIdx = 0, bitIdx = 7;
bool upward = true;
int col = size - 1;
while (col > 0)
{
if (col == 6) col--;
for (int rowStep = 0; rowStep < size; rowStep++)
{
int row = upward ? size - 1 - rowStep : rowStep;
for (int c = 0; c < 2; c++)
{
int cc = col - c;
if (r[row, cc]) continue;
bool bit = cwIdx < cw.Length && ((cw[cwIdx] >> bitIdx) & 1) == 1;
m[row, cc] = bit;
if (--bitIdx < 0)
{
bitIdx = 7;
cwIdx++;
}
}
}
upward = !upward;
col -= 2;
}
}
static void ApplyMask(bool[,] m, bool[,] r, int mask, int size)
{
for (int row = 0; row < size; row++)
for (int col = 0; col < size; col++)
{
if (r[row, col]) continue;
bool flip = mask switch
{
0 => (row + col) % 2 == 0,
1 => row % 2 == 0,
2 => col % 3 == 0,
3 => (row + col) % 3 == 0,
4 => (row / 2 + col / 3) % 2 == 0,
5 => row * col % 2 + row * col % 3 == 0,
6 => (row * col % 2 + row * col % 3) % 2 == 0,
7 => ((row + col) % 2 + row * col % 3) % 2 == 0,
_ => false
};
if (flip) m[row, col] ^= true;
}
}
static readonly ushort[] _formatL = { 0x77C4, 0x72F3, 0x7DAA, 0x789D, 0x662F, 0x6318, 0x6C41, 0x6976 };
static readonly ushort[] _formatM = { 0x5412, 0x5125, 0x5E7C, 0x5B4B, 0x45F9, 0x40CE, 0x4F97, 0x4AA0 };
static readonly ushort[] _formatQ = { 0x355F, 0x3068, 0x3F31, 0x3A06, 0x24B4, 0x2183, 0x2EDA, 0x2BED };
static readonly ushort[] _formatH = { 0x1689, 0x13BE, 0x1CE7, 0x19D0, 0x0762, 0x0255, 0x0D0C, 0x083B };
static void ApplyFormatInfo(bool[,] m, bool[,] r, int eccIndex, int mask, int size)
{
ushort[] table = eccIndex switch { 0 => _formatM, 1 => _formatL, 2 => _formatH, _ => _formatQ };
ushort fmt = table[mask];
int[] seq = { 0, 1, 2, 3, 4, 5, 7, 8 };
for (int i = 0; i < 8; i++)
{
bool bit = ((fmt >> (14 - i)) & 1) == 1;
m[8, seq[i]] = bit;
m[seq[i < 6 ? i : i + 1 < 8 ? i : i], 8] = bit;
}
for (int i = 0; i < 7; i++)
{
bool bit = ((fmt >> i) & 1) == 1;
m[size - 1 - i, 8] = bit;
}
for (int i = 7; i < 15; i++)
{
bool bit = ((fmt >> i) & 1) == 1;
m[8, size - 15 + i] = bit;
}
}
static int CalcPenalty(bool[,] m, int size)
{
int p = 0;
for (int r = 0; r < size; r++)
{
int run = 1;
for (int c = 1; c < size; c++)
{
if (m[r, c] == m[r, c - 1])
{
run++;
if (run == 5) p += 3;
else if (run > 5) p++;
}
else run = 1;
}
run = 1;
for (int c = 1; c < size; c++)
{
if (m[c, r] == m[c - 1, r])
{
run++;
if (run == 5) p += 3;
else if (run > 5) p++;
}
else run = 1;
}
}
for (int r = 0; r < size - 1; r++)
for (int c = 0; c < size - 1; c++)
if (m[r, c] == m[r, c + 1] && m[r, c] == m[r + 1, c] && m[r, c] == m[r + 1, c + 1])
p += 3;
return p;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 6510d029572264a5b8aea6d31e302686

View File

@@ -1,67 +0,0 @@
using System;
namespace Darkmatter.Fonepay
{
public static partial class FonepayQRGenerator
{
static readonly byte[] _exp = new byte[512];
static readonly byte[] _log = new byte[256];
static FonepayQRGenerator()
{
int x = 1;
for (int i = 0; i < 255; i++)
{
_exp[i] = (byte)x;
_log[x] = (byte)i;
x <<= 1;
if (x >= 256) x ^= 0x11d;
}
for (int i = 255; i < 512; i++) _exp[i] = _exp[i - 255];
}
static byte GfMul(byte a, byte b)
{
if (a == 0 || b == 0) return 0;
return _exp[(_log[a] + _log[b]) % 255];
}
static byte GfPow(int b, int e) => _exp[(_log[(byte)b] * e) % 255];
static byte[] ReedSolomon(byte[] data, int ecCount)
{
byte[] gen = RsGenerator(ecCount);
byte[] msg = new byte[data.Length + ecCount];
Array.Copy(data, msg, data.Length);
for (int i = 0; i < data.Length; i++)
{
byte coef = msg[i];
if (coef == 0) continue;
for (int j = 1; j < gen.Length; j++)
msg[i + j] ^= GfMul(gen[j], coef);
}
byte[] ec = new byte[ecCount];
Array.Copy(msg, data.Length, ec, 0, ecCount);
return ec;
}
static byte[] RsGenerator(int degree)
{
byte[] g = { 1 };
for (int i = 0; i < degree; i++)
{
byte[] ng = new byte[g.Length + 1];
byte root = GfPow(2, i);
for (int j = 0; j < g.Length; j++)
{
ng[j] ^= GfMul(g[j], root);
ng[j + 1] ^= g[j];
}
g = ng;
}
return g;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 049bdafac777b4e58bafafb2224e88a8

View File

@@ -1,11 +1,13 @@
using System.Collections;
using QRCoder;
using UnityEngine;
namespace Darkmatter.Fonepay
{
/// <summary>
/// Pure C# QR Code generator. Byte mode, ECC L/M/Q/H, versions 110.
/// QR code generator backed by QRCoder (MIT). See Plugins/QRCoder-LICENSE.txt.
/// </summary>
public static partial class FonepayQRGenerator
public static class FonepayQRGenerator
{
public enum EccLevel { L, M, Q, H }
@@ -25,16 +27,21 @@ namespace Darkmatter.Fonepay
Color dark = darkColor ?? Color.black;
Color light = lightColor ?? Color.white;
var pixels = new Color[texSize * texSize];
for (int row = 0; row < size; row++)
for (int col = 0; col < size; col++)
{
Color c = matrix[row, col] ? dark : light;
int yBase = (size - 1 - row) * pixelSize;
int xBase = col * pixelSize;
for (int py = 0; py < pixelSize; py++)
{
int rowStart = (yBase + py) * texSize + xBase;
for (int px = 0; px < pixelSize; px++)
tex.SetPixel(col * pixelSize + px,
(size - 1 - row) * pixelSize + py, c);
pixels[rowStart + px] = c;
}
}
tex.SetPixels(pixels);
tex.Apply();
return tex;
}
@@ -57,10 +64,26 @@ namespace Darkmatter.Fonepay
{
if (string.IsNullOrEmpty(text))
throw new System.ArgumentException("QR text must be non-empty", nameof(text));
byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
var (version, ecBlocks) = ChooseVersion(data.Length, ecc);
byte[] codewords = BuildCodewords(data, version, ecc, ecBlocks);
return BuildMatrix(version, codewords);
}
using var data = new QRCodeGenerator().CreateQrCode(text, MapEcc(ecc), forceUtf8: true);
int size = data.ModuleMatrix.Count;
var matrix = new bool[size, size];
for (int row = 0; row < size; row++)
{
BitArray bits = data.ModuleMatrix[row];
for (int col = 0; col < size; col++)
matrix[row, col] = bits[col];
}
return matrix;
}
static QRCodeGenerator.ECCLevel MapEcc(EccLevel e) => e switch
{
EccLevel.L => QRCodeGenerator.ECCLevel.L,
EccLevel.M => QRCodeGenerator.ECCLevel.M,
EccLevel.Q => QRCodeGenerator.ECCLevel.Q,
EccLevel.H => QRCodeGenerator.ECCLevel.H,
_ => QRCodeGenerator.ECCLevel.M
};
}
}