initital push
This commit is contained in:
8
Packages/com.voidbotz.fonepayunity/Runtime/API.meta
Normal file
8
Packages/com.voidbotz.fonepayunity/Runtime/API.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfc7d75f72cd74bfeb9c54fc8c8675bc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP layer. Auto-injects merchantCode/username/password from FonepayConfigSO and
|
||||
/// computes the HMAC SHA-512 dataValidation before each request. Callers never set
|
||||
/// credentials on request structs.
|
||||
/// </summary>
|
||||
internal sealed class FonepayApiClient
|
||||
{
|
||||
private const string QrPath = "merchant/merchantDetailsForThirdParty/thirdPartyDynamicQrDownload";
|
||||
private const string StatusPath = "merchant/merchantDetailsForThirdParty/thirdPartyDynamicQrGetStatus";
|
||||
private const string TaxRefundPath = "merchant/merchantDetailsForThirdParty/taxRefund";
|
||||
|
||||
private readonly FonepayConfigSO _config;
|
||||
private readonly HmacSha512Signer _signer;
|
||||
|
||||
internal FonepayApiClient(FonepayConfigSO config, HmacSha512Signer signer)
|
||||
{
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
_signer = signer ?? throw new ArgumentNullException(nameof(signer));
|
||||
}
|
||||
|
||||
// ── public API ────────────────────────────────────────────────────
|
||||
|
||||
internal Task<QrResponse> PostQRAsync(QrRequest request, CancellationToken ct)
|
||||
{
|
||||
var amountStr = request.amount.ToString("0.00", CultureInfo.InvariantCulture);
|
||||
var payload = new QrRequestPayload
|
||||
{
|
||||
amount = request.amount,
|
||||
prn = request.prn,
|
||||
remarks1 = request.remarks1,
|
||||
remarks2 = request.remarks2,
|
||||
pm = request.pm,
|
||||
merchantCode = _config.MerchantCode,
|
||||
username = _config.Username,
|
||||
password = _config.GetPassword(),
|
||||
dataValidation = _signer.SignQrRequest(
|
||||
amountStr, request.prn, _config.MerchantCode,
|
||||
request.remarks1, request.remarks2),
|
||||
};
|
||||
return SendPostAsync<QrResponse>(QrPath, payload, ct);
|
||||
}
|
||||
|
||||
internal Task<QrResponse> GetStatusAsync(string prn, CancellationToken ct)
|
||||
{
|
||||
var sig = _signer.SignStatusCheck(prn, _config.MerchantCode);
|
||||
var url = $"{_config.ResolveBaseUrl()}{StatusPath}" +
|
||||
$"?prn={UnityWebRequest.EscapeURL(prn)}" +
|
||||
$"&merchantCode={UnityWebRequest.EscapeURL(_config.MerchantCode)}" +
|
||||
$"&dataValidation={UnityWebRequest.EscapeURL(sig)}" +
|
||||
$"&username={UnityWebRequest.EscapeURL(_config.Username)}" +
|
||||
$"&password={UnityWebRequest.EscapeURL(_config.GetPassword())}";
|
||||
return SendAsync<QrResponse>(url, UnityWebRequest.kHttpVerbGET, null, ct);
|
||||
}
|
||||
|
||||
internal Task<TaxRefundResponse> PostTaxRefundAsync(TaxRefundRequest r, CancellationToken ct)
|
||||
{
|
||||
var invoiceDate = r.invoiceDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
var amountStr = r.transactionAmount.ToString("0.00", CultureInfo.InvariantCulture);
|
||||
var payload = new TaxRefundRequestPayload
|
||||
{
|
||||
fonepayTraceId = r.fonepayTraceId,
|
||||
merchantPRN = r.merchantPRN,
|
||||
invoiceNumber = r.invoiceNumber,
|
||||
invoiceDate = invoiceDate,
|
||||
transactionAmount = r.transactionAmount,
|
||||
merchantCode = _config.MerchantCode,
|
||||
username = _config.Username,
|
||||
password = _config.GetPassword(),
|
||||
dataValidation = _signer.SignTaxRefund(
|
||||
r.fonepayTraceId, r.merchantPRN, r.invoiceNumber,
|
||||
invoiceDate, amountStr, _config.MerchantCode),
|
||||
};
|
||||
return SendPostAsync<TaxRefundResponse>(TaxRefundPath, payload, ct);
|
||||
}
|
||||
|
||||
// ── transport ─────────────────────────────────────────────────────
|
||||
|
||||
private Task<T> SendPostAsync<T>(string relativePath, object body, CancellationToken ct)
|
||||
{
|
||||
var url = _config.ResolveBaseUrl() + relativePath;
|
||||
var json = JsonUtility.ToJson(body);
|
||||
return SendAsync<T>(url, UnityWebRequest.kHttpVerbPOST, json, ct);
|
||||
}
|
||||
|
||||
private static Task<T> SendAsync<T>(string url, string method, string jsonBody, CancellationToken ct)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var req = new UnityWebRequest(url, method)
|
||||
{
|
||||
downloadHandler = new DownloadHandlerBuffer(),
|
||||
};
|
||||
if (!string.IsNullOrEmpty(jsonBody))
|
||||
{
|
||||
req.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(jsonBody));
|
||||
req.SetRequestHeader("Content-Type", "application/json");
|
||||
}
|
||||
req.SetRequestHeader("Accept", "application/json");
|
||||
|
||||
CancellationTokenRegistration reg = default;
|
||||
if (ct.CanBeCanceled)
|
||||
reg = ct.Register(() => { try { req.Abort(); } catch { /* ignored */ } });
|
||||
|
||||
var op = req.SendWebRequest();
|
||||
op.completed += _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
tcs.TrySetCanceled(ct);
|
||||
return;
|
||||
}
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
var failed = req.result != UnityWebRequest.Result.Success;
|
||||
#else
|
||||
var failed = req.isHttpError || req.isNetworkError;
|
||||
#endif
|
||||
if (failed)
|
||||
{
|
||||
tcs.TrySetException(new FonepayError(
|
||||
(int)req.responseCode,
|
||||
$"HTTP {(int)req.responseCode} {req.error}: {req.downloadHandler?.text}",
|
||||
url));
|
||||
return;
|
||||
}
|
||||
|
||||
var text = req.downloadHandler.text ?? string.Empty;
|
||||
T result;
|
||||
try { result = JsonUtility.FromJson<T>(text); }
|
||||
catch (Exception e)
|
||||
{
|
||||
tcs.TrySetException(new FonepayError(0,
|
||||
$"JSON parse failed: {e.Message}. Body: {text}", url));
|
||||
return;
|
||||
}
|
||||
tcs.TrySetResult(result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
reg.Dispose();
|
||||
req.Dispose();
|
||||
}
|
||||
};
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3dfb04f3804724b2d92e02816ddc53f2
|
||||
@@ -0,0 +1,19 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
public class FonepayWebsocketClient : MonoBehaviour
|
||||
{
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58200a692447f4f10a39df731fa8a521
|
||||
8
Packages/com.voidbotz.fonepayunity/Runtime/Async.meta
Normal file
8
Packages/com.voidbotz.fonepayunity/Runtime/Async.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bae1e38d737bf4e26ac9a9e3c76f6b25
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
#if UNITASK_SUPPORT
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
public readonly struct FonepayAsync<T>
|
||||
{
|
||||
private readonly UniTask<T> _task;
|
||||
internal FonepayAsync(UniTask<T> task) => _task = task;
|
||||
|
||||
// UniTask<T>.Awaiter is a public named type — no problem
|
||||
public UniTask<T>.Awaiter GetAwaiter() => _task.GetAwaiter();
|
||||
}
|
||||
|
||||
public readonly struct FonepayAsync
|
||||
{
|
||||
private readonly UniTask _task;
|
||||
internal FonepayAsync(UniTask task) => _task = task;
|
||||
|
||||
public UniTask.Awaiter GetAwaiter() => _task.GetAwaiter();
|
||||
}
|
||||
|
||||
#else
|
||||
// Unity 2023.1+ path — Awaitable<T>.GetAwaiter() return type is internal,
|
||||
// so we store Task<T> and convert lazily via an async wrapper.
|
||||
// The wrapper's return type is inferred — we never name the awaiter.
|
||||
|
||||
public readonly struct FonepayAsync<T>
|
||||
{
|
||||
private readonly System.Threading.Tasks.Task<T> _task;
|
||||
internal FonepayAsync(System.Threading.Tasks.Task<T> task) => _task = task;
|
||||
|
||||
// Return type inferred from the async method — compiler handles it
|
||||
public System.Runtime.CompilerServices.TaskAwaiter<T> GetAwaiter()
|
||||
=> _task.GetAwaiter();
|
||||
}
|
||||
|
||||
public readonly struct FonepayAsync
|
||||
{
|
||||
private readonly System.Threading.Tasks.Task _task;
|
||||
internal FonepayAsync(System.Threading.Tasks.Task task) => _task = task;
|
||||
|
||||
public System.Runtime.CompilerServices.TaskAwaiter GetAwaiter()
|
||||
=> _task.GetAwaiter();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e222c5804d46c4c138b5a413587344f4
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#if UNITASK_SUPPORT
|
||||
using Cysharp.Threading.Tasks;
|
||||
#endif
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
internal static class FonepayAsyncBridge
|
||||
{
|
||||
#if UNITASK_SUPPORT
|
||||
internal static FonepayAsync<T> Wrap<T>(Task<T> task, CancellationToken ct = default)
|
||||
=> new FonepayAsync<T>(task.AsUniTask().AttachExternalCancellation(ct));
|
||||
|
||||
internal static FonepayAsync Wrap(Task task, CancellationToken ct = default)
|
||||
=> new FonepayAsync(task.AsUniTask().AttachExternalCancellation(ct));
|
||||
#else
|
||||
// Task is already awaitable — wrap directly, no Awaitable needed
|
||||
internal static FonepayAsync<T> Wrap<T>(Task<T> task, CancellationToken ct = default)
|
||||
=> new FonepayAsync<T>(task);
|
||||
|
||||
internal static FonepayAsync Wrap(Task task, CancellationToken ct = default)
|
||||
=> new FonepayAsync(task);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 778cc7d92ed1645c195685b07ca02ee1
|
||||
8
Packages/com.voidbotz.fonepayunity/Runtime/Core.meta
Normal file
8
Packages/com.voidbotz.fonepayunity/Runtime/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5ca890ee6ddf43ebb0a47f228ffe838
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
/// <summary>
|
||||
/// Transient ScriptableObject containing baked secrets for player builds.
|
||||
/// Created by FonepayBuildSecretsInjector before build, deleted after build completes.
|
||||
/// Never commit this asset — added to .gitignore by the settings window.
|
||||
/// </summary>
|
||||
public class FonepayBakedSecrets : ScriptableObject
|
||||
{
|
||||
public string password;
|
||||
public string secretKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6baad01d5e6740bfb8abc8faadfe594
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
/// <summary>
|
||||
/// Public facade. Auto-loads config + secrets via <see cref="FonepayConfig.Load"/>.
|
||||
/// Caller never touches credentials.
|
||||
/// </summary>
|
||||
public sealed class FonepayClient
|
||||
{
|
||||
private readonly FonepayApiClient _api;
|
||||
|
||||
public FonepayClient() : this(FonepayConfig.Load())
|
||||
{
|
||||
}
|
||||
|
||||
public FonepayClient(FonepayConfigSO config)
|
||||
{
|
||||
var signer = new HmacSha512Signer(config.GetSecretKey());
|
||||
_api = new FonepayApiClient(config, signer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request a new QR code. The returned URL is valid for 15 minutes. Call GetStatusAsync() to check if the QR code has been paid.
|
||||
/// </summary>
|
||||
/// <param name="req"></param>
|
||||
/// <param name="ct"></param>
|
||||
/// <returns></returns>
|
||||
public Task<QrResponse> PostQRAsync(QrRequest req, CancellationToken ct = default)
|
||||
=> _api.PostQRAsync(req, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Check if a QR code has been paid. Call after PostQRAsync() to check if the QR code has been paid. Returns "PAID" if successful, "UNPAID" if not yet paid, or an error message if the PRN is invalid or expired.
|
||||
/// </summary>
|
||||
/// <param name="prn"></param>
|
||||
/// <param name="ct"></param>
|
||||
/// <returns></returns>
|
||||
public Task<QrResponse> GetStatusAsync(string prn, CancellationToken ct = default)
|
||||
=> _api.GetStatusAsync(prn, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Request a tax refund. Returns "REFUND_SUCCESS" if successful, or an error message if the PRN is invalid, expired, or not eligible for refund.
|
||||
/// </summary>
|
||||
/// <param name="req"></param>
|
||||
/// <param name="ct"></param>
|
||||
/// <returns></returns>
|
||||
public Task<TaxRefundResponse> PostTaxRefundAsync(TaxRefundRequest req, CancellationToken ct = default)
|
||||
=> _api.PostTaxRefundAsync(req, ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1ab3a2c9f1aa4d5aa87b6305323a1fc
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
/// <summary>
|
||||
/// Static accessor. Returns ready-to-use <see cref="FonepayConfigSO"/> with credentials injected.
|
||||
/// Editor: secrets supplied via <see cref="EditorSecretsProvider"/> hook (set by FonepaySecretsStore on editor load).
|
||||
/// Builds: secrets read from <see cref="FonepayBakedSecrets"/> resource (written by build preprocessor, removed after build).
|
||||
/// </summary>
|
||||
public static class FonepayConfig
|
||||
{
|
||||
private const string ResourcePath = "FonepayConfig";
|
||||
private const string BakedSecretsResource = "FonepayBakedSecrets";
|
||||
|
||||
public static Func<(string password, string secretKey)> EditorSecretsProvider;
|
||||
|
||||
private static FonepayConfigSO _cached;
|
||||
|
||||
public static FonepayConfigSO Load()
|
||||
{
|
||||
if (_cached != null) return _cached;
|
||||
|
||||
var so = Resources.Load<FonepayConfigSO>(ResourcePath);
|
||||
if (so == null)
|
||||
throw new FonepayError(0,
|
||||
"FonepayConfig asset missing. Open Tools > Fonepay > Settings to create it.",
|
||||
"FonepayConfig.Load");
|
||||
|
||||
string password = null;
|
||||
string secretKey = null;
|
||||
|
||||
if (Application.isEditor && EditorSecretsProvider != null)
|
||||
{
|
||||
var creds = EditorSecretsProvider();
|
||||
password = creds.password;
|
||||
secretKey = creds.secretKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
var baked = Resources.Load<FonepayBakedSecrets>(BakedSecretsResource);
|
||||
if (baked != null)
|
||||
{
|
||||
password = baked.password;
|
||||
secretKey = baked.secretKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(secretKey))
|
||||
throw new FonepayError(0,
|
||||
"Fonepay credentials missing. Open Tools > Fonepay > Settings.",
|
||||
"FonepayConfig.Load");
|
||||
|
||||
so.SetCredentials(password, secretKey);
|
||||
_cached = so;
|
||||
return so;
|
||||
}
|
||||
|
||||
public static void Invalidate() => _cached = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3617f631547ae4b10a0f8f634751c61f
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
// <summary>
|
||||
// ScriptableObject. Holds non-secret config fields serialised to disk. Secret key and password are injected at runtime only — never serialised.
|
||||
// </summary>
|
||||
public class FonepayConfigSO : ScriptableObject
|
||||
{
|
||||
[SerializeField] private FonepayEnvironment environment;
|
||||
[SerializeField] private string merchantCode;
|
||||
[SerializeField] private string username;
|
||||
|
||||
private string _password;
|
||||
private string _secretKey;
|
||||
private bool _credentialsSet;
|
||||
|
||||
// ── public read access ────────────────────────────────────────────
|
||||
|
||||
public FonepayEnvironment Environment => environment;
|
||||
public string MerchantCode => merchantCode;
|
||||
public string Username => username;
|
||||
|
||||
// ── runtime credential injection (called by FonepayPlayModeInjector) ──
|
||||
|
||||
public void SetCredentials(string password, string secretKey)
|
||||
{
|
||||
_password = password;
|
||||
_secretKey = secretKey;
|
||||
_credentialsSet = true;
|
||||
}
|
||||
|
||||
private const string FonepayLiveEndpoint = "https://merchantapi.fonepay.com/api/";
|
||||
private const string FonepayDevEndpoint = "https://dev-merchantapi.fonepay.com/api/";
|
||||
|
||||
// ── used internally by FonepayApiClient and HmacSha512Signer ─────
|
||||
|
||||
internal string GetPassword() => GuardCredentials(_password);
|
||||
internal string GetSecretKey() => GuardCredentials(_secretKey);
|
||||
|
||||
// ── URL resolution ────────────────────────────────────────────────
|
||||
|
||||
public string ResolveBaseUrl() => environment == FonepayEnvironment.Live
|
||||
? FonepayLiveEndpoint
|
||||
: FonepayDevEndpoint;
|
||||
|
||||
// ── guards ────────────────────────────────────────────────────────
|
||||
|
||||
private string GuardCredentials(string value)
|
||||
{
|
||||
if (!_credentialsSet)
|
||||
throw new FonepayError(0,
|
||||
"Credentials not set. Call SetCredentials() before using FonepayClient.",
|
||||
"FonepayConfig.SetCredentials");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba80c86a04c584a649c6d31e8f10adad
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
public enum FonepayEnvironment
|
||||
{
|
||||
Dev,
|
||||
Live
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c92e6dd7cf3a84c1e85fa2f19741794e
|
||||
8
Packages/com.voidbotz.fonepayunity/Runtime/Crypto.meta
Normal file
8
Packages/com.voidbotz.fonepayunity/Runtime/Crypto.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74b5b5675851d481a89f7c6136b159d7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
internal sealed class HmacSha512Signer
|
||||
{
|
||||
private readonly byte[] _keyBytes;
|
||||
|
||||
internal HmacSha512Signer(string secretKey)
|
||||
{
|
||||
_keyBytes = Encoding.UTF8.GetBytes(secretKey);
|
||||
}
|
||||
|
||||
internal string SignQrRequest(
|
||||
string amount, string prn, string merchantCode,
|
||||
string remarks1, string remarks2)
|
||||
=> Compute($"{amount},{prn},{merchantCode},{remarks1},{remarks2}");
|
||||
|
||||
internal string SignQrRequestWithTax(
|
||||
string amount, string prn, string merchantCode,
|
||||
string remarks1, string remarks2,
|
||||
string taxAmount, string taxRefund)
|
||||
=> Compute($"{amount},{prn},{merchantCode},{remarks1},{remarks2},{taxAmount},{taxRefund}");
|
||||
|
||||
internal string SignStatusCheck(string prn, string merchantCode)
|
||||
=> Compute($"{prn},{merchantCode}");
|
||||
|
||||
internal string SignTaxRefund(
|
||||
string fonepayTraceId, string merchantPrn,
|
||||
string invoiceNumber, string invoiceDate,
|
||||
string transactionAmount, string merchantCode)
|
||||
=> Compute($"{fonepayTraceId},{merchantPrn},{invoiceNumber},{invoiceDate},{transactionAmount},{merchantCode}");
|
||||
|
||||
private string Compute(string message)
|
||||
{
|
||||
using var hmac = new HMACSHA512(_keyBytes);
|
||||
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
|
||||
return BytesToHexLower(hash);
|
||||
}
|
||||
|
||||
private static string BytesToHexLower(byte[] bytes)
|
||||
{
|
||||
var sb = new StringBuilder(bytes.Length * 2);
|
||||
foreach (byte b in bytes)
|
||||
sb.Append(b.ToString("x2"));
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 93fb47df723f74372b0fa6e228e4dd73
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Darkmatter.FonepayUnity",
|
||||
"rootNamespace": "Darkmatter.Fonepay",
|
||||
"references": [
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [
|
||||
{
|
||||
"name": "com.cysharp.unitask",
|
||||
"expression": "",
|
||||
"define": "UNITASK_SUPPORT"
|
||||
}
|
||||
],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de841957485ec4208a629f66aa4b24c9
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.voidbotz.fonepayunity/Runtime/Models.meta
Normal file
8
Packages/com.voidbotz.fonepayunity/Runtime/Models.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c216a6be596b84649a99f22d45ca0ce5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
public sealed class FonepayError : Exception
|
||||
{
|
||||
public int ErrorCode { get; }
|
||||
|
||||
public FonepayError(int errorCode, string message,
|
||||
string docs)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46adc238f3552442bad6f68d9b2ee68a
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
/// <summary>
|
||||
/// User-facing QR request. Credentials (merchantCode/username/password) and the
|
||||
/// HMAC dataValidation are injected by FonepayApiClient at send time.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct QrRequest
|
||||
{
|
||||
public float amount;
|
||||
public string prn;
|
||||
public string remarks1;
|
||||
public string remarks2;
|
||||
public string pm;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
internal struct QrRequestPayload
|
||||
{
|
||||
public float amount;
|
||||
public string prn;
|
||||
public string remarks1;
|
||||
public string remarks2;
|
||||
public string pm;
|
||||
public string merchantCode;
|
||||
public string dataValidation;
|
||||
public string username;
|
||||
public string password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 255c2e13d0ed54987904cd65a5349d29
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
[Serializable]
|
||||
public struct QrResponse
|
||||
{
|
||||
public string message;
|
||||
public string qrMessage;
|
||||
public string status;
|
||||
public int statusCode;
|
||||
public bool success;
|
||||
public string thirdpartyQrWebSocketUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 405247a27c684f08ab11d6ea33d250c0
|
||||
timeCreated: 1778138521
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
/// <summary>
|
||||
/// User-facing tax refund request. Credentials and HMAC dataValidation are
|
||||
/// injected by FonepayApiClient at send time.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct TaxRefundRequest
|
||||
{
|
||||
public string fonepayTraceId;
|
||||
public string merchantPRN;
|
||||
public string invoiceNumber;
|
||||
public DateTime invoiceDate;
|
||||
public float transactionAmount;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
internal struct TaxRefundRequestPayload
|
||||
{
|
||||
public string fonepayTraceId;
|
||||
public string merchantPRN;
|
||||
public string invoiceNumber;
|
||||
public string invoiceDate;
|
||||
public float transactionAmount;
|
||||
public string merchantCode;
|
||||
public string dataValidation;
|
||||
public string username;
|
||||
public string password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 328f1e46b5a941bbbf5c0e8294fc9c34
|
||||
timeCreated: 1778140313
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
public struct TaxRefundResponse
|
||||
{
|
||||
public string fonepayTraceId;
|
||||
public string message;
|
||||
public bool success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ccdcd51d0dc843eabdb25aa3312bd91a
|
||||
timeCreated: 1778140771
|
||||
8
Packages/com.voidbotz.fonepayunity/Runtime/QR.meta
Normal file
8
Packages/com.voidbotz.fonepayunity/Runtime/QR.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47a1e64b2c84a4238b5cc62aa4c8fb80
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,40 @@
|
||||
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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a611e576dae8b4ff6b15c314c1246cf1
|
||||
@@ -0,0 +1,62 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e67574ccbe68944928d706bd99d785ca
|
||||
@@ -0,0 +1,234 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6510d029572264a5b8aea6d31e302686
|
||||
@@ -0,0 +1,67 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 049bdafac777b4e58bafafb2224e88a8
|
||||
@@ -0,0 +1,64 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Fonepay
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure C# QR Code generator. Byte mode, ECC L/M/Q/H, versions 1–10.
|
||||
/// </summary>
|
||||
public static partial class FonepayQRGenerator
|
||||
{
|
||||
public enum EccLevel { L, M, Q, H }
|
||||
|
||||
public static Texture2D GenerateTexture(string text, int pixelSize = 10,
|
||||
EccLevel ecc = EccLevel.M, Color? darkColor = null, Color? lightColor = null)
|
||||
{
|
||||
bool[,] matrix = GenerateMatrix(text, ecc);
|
||||
int size = matrix.GetLength(0);
|
||||
int texSize = size * pixelSize;
|
||||
|
||||
var tex = new Texture2D(texSize, texSize, TextureFormat.RGBA32, false)
|
||||
{
|
||||
filterMode = FilterMode.Point,
|
||||
wrapMode = TextureWrapMode.Clamp
|
||||
};
|
||||
|
||||
Color dark = darkColor ?? Color.black;
|
||||
Color light = lightColor ?? Color.white;
|
||||
|
||||
for (int row = 0; row < size; row++)
|
||||
for (int col = 0; col < size; col++)
|
||||
{
|
||||
Color c = matrix[row, col] ? dark : light;
|
||||
for (int py = 0; py < pixelSize; py++)
|
||||
for (int px = 0; px < pixelSize; px++)
|
||||
tex.SetPixel(col * pixelSize + px,
|
||||
(size - 1 - row) * pixelSize + py, c);
|
||||
}
|
||||
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
|
||||
public static Sprite GenerateSprite(string text, int pixelSize = 10,
|
||||
EccLevel ecc = EccLevel.M, Color? darkColor = null, Color? lightColor = null,
|
||||
float pixelsPerUnit = 100f)
|
||||
{
|
||||
var tex = GenerateTexture(text, pixelSize, ecc, darkColor, lightColor);
|
||||
var sprite = Sprite.Create(tex,
|
||||
new Rect(0, 0, tex.width, tex.height),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
pixelsPerUnit,
|
||||
0, SpriteMeshType.FullRect);
|
||||
sprite.name = "FonepayQR";
|
||||
return sprite;
|
||||
}
|
||||
|
||||
public static bool[,] GenerateMatrix(string text, EccLevel ecc = EccLevel.M)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 926e00b59f70b418bbf937c11a1c8494
|
||||
Reference in New Issue
Block a user