Compare commits
11 Commits
906ebbcac9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b609c43199 | ||
|
|
ac61fdba83 | ||
|
|
98526d82d9 | ||
|
|
f7f287bca7 | ||
|
|
5515a88cf5 | ||
|
|
028a1a7f46 | ||
|
|
dface29ff2 | ||
|
|
6a8a6e46f0 | ||
|
|
9f620084b2 | ||
|
|
3c17829453 | ||
|
|
846a4fda9c |
22
.github/workflows/upm-split.yml
vendored
Normal file
22
.github/workflows/upm-split.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Publish UPM
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: ['Packages/com.darkmattergameproduction.fonepay-unity/**']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
split:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Split subtree to upm branch
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git subtree split --prefix=Packages/com.darkmattergameproduction.fonepay-unity -b upm
|
||||||
|
git push origin upm --force
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -105,3 +105,4 @@ InitTestScene*.unity*
|
|||||||
# Auto-generated cache in Assets folder
|
# Auto-generated cache in Assets folder
|
||||||
/[Aa]ssets/[Ss]ceneDependencyCache*
|
/[Aa]ssets/[Ss]ceneDependencyCache*
|
||||||
Assets/Resources/FonepayBakedSecrets.asset*
|
Assets/Resources/FonepayBakedSecrets.asset*
|
||||||
|
Assets/Resources/FonepayConfig.asset*
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Cysharp.Threading.Tasks;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
|
|
||||||
namespace Darkmatter.Fonepay.Samples
|
|
||||||
{
|
|
||||||
public class SamplePayment : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField] private Image qrImage;
|
|
||||||
[SerializeField] private GameObject successObject;
|
|
||||||
[SerializeField] private GameObject failedObject;
|
|
||||||
[SerializeField] private Button payButton;
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
payButton.onClick.AddListener(OnPayButtonClicked);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPayButtonClicked()
|
|
||||||
{
|
|
||||||
InitiatePayment().Forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async UniTask InitiatePayment()
|
|
||||||
{
|
|
||||||
var fonepay = new FonepayClient();
|
|
||||||
var request = new QrRequest
|
|
||||||
{
|
|
||||||
amount = 1,
|
|
||||||
remarks1 = "mausham ko paisa"
|
|
||||||
};
|
|
||||||
var qr = await fonepay.PurchaseAsync(request, destroyCancellationToken);
|
|
||||||
|
|
||||||
if (qr.qrCode != null)
|
|
||||||
{
|
|
||||||
qrImage.sprite = Sprite.Create(
|
|
||||||
qr.qrCode,
|
|
||||||
new Rect(0, 0, qr.qrCode.width, qr.qrCode.height),
|
|
||||||
new Vector2(0.5f, 0.5f));
|
|
||||||
qrImage.gameObject.SetActive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var payment = await fonepay.AwaitPaymentAsync(
|
|
||||||
qr.thirdpartyQrWebSocketUrl,
|
|
||||||
onQrVerified: v => Debug.Log($"Fonepay QR verified: {v}"),
|
|
||||||
ct: destroyCancellationToken);
|
|
||||||
|
|
||||||
var ok = payment.Outcome == PaymentOutcome.Complete;
|
|
||||||
|
|
||||||
qrImage.gameObject.SetActive(false);
|
|
||||||
successObject.SetActive(ok);
|
|
||||||
failedObject.SetActive(!ok);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 57da22305386447fcabd663378359ede
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 111b87cef6134401aa05813da47709aa
|
guid: 5dff20c6950e54d71bfe971186191b01
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 7f9dbf7477f88405ca98d50075ced835
|
guid: 2b7e8dee7532643a181e55ce459fef09
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
8
Assets/Samples/Fonepay Unity/0.2.0.meta
Normal file
8
Assets/Samples/Fonepay Unity/0.2.0.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8231582f6900a44d2a9868b5baad6101
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/Samples/Fonepay Unity/0.2.0/.DS_Store
vendored
Normal file
BIN
Assets/Samples/Fonepay Unity/0.2.0/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 314c0fedd56f54fb893743926389db98
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/Samples/Fonepay Unity/0.2.0/Example Payment Flow/.DS_Store
vendored
Normal file
BIN
Assets/Samples/Fonepay Unity/0.2.0/Example Payment Flow/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace Darkmatter.Fonepay.Samples
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal end-to-end sample: request QR, render to Image, await payment,
|
||||||
|
/// support cancellation via a cancel button.
|
||||||
|
/// </summary>
|
||||||
|
public class SamplePayment : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private Image qrImage;
|
||||||
|
[SerializeField] private GameObject successObject;
|
||||||
|
[SerializeField] private GameObject failedObject;
|
||||||
|
[SerializeField] private Button payButton;
|
||||||
|
[SerializeField] private Button cancelButton;
|
||||||
|
[SerializeField] private float amount = 1f;
|
||||||
|
|
||||||
|
private CancellationTokenSource _payCts;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
payButton.onClick.AddListener(() => InitiatePayment().Forget());
|
||||||
|
if (cancelButton != null)
|
||||||
|
cancelButton.onClick.AddListener(() => _payCts?.Cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UniTask InitiatePayment()
|
||||||
|
{
|
||||||
|
if (_payCts != null) return;
|
||||||
|
|
||||||
|
payButton.interactable = false;
|
||||||
|
var fonepay = new FonepayClient();
|
||||||
|
var cts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
_payCts = cts;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var qr = await fonepay.PurchaseAsync(
|
||||||
|
new QrRequest { amount = amount, remarks1 = "sample" },
|
||||||
|
cts.Token);
|
||||||
|
|
||||||
|
if (qr.qrCode != null)
|
||||||
|
{
|
||||||
|
qrImage.sprite = Sprite.Create(
|
||||||
|
qr.qrCode,
|
||||||
|
new Rect(0, 0, qr.qrCode.width, qr.qrCode.height),
|
||||||
|
new Vector2(0.5f, 0.5f));
|
||||||
|
qrImage.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var payment = await fonepay.AwaitPaymentAsync(
|
||||||
|
qr.thirdpartyQrWebSocketUrl,
|
||||||
|
onQrVerified: v => Debug.Log($"Fonepay QR verified: {v}"),
|
||||||
|
ct: cts.Token);
|
||||||
|
|
||||||
|
Debug.Log($"Payment frame: {JsonUtility.ToJson(payment)}");
|
||||||
|
ShowResult(payment.Outcome == PaymentOutcome.Complete);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Debug.Log("Payment cancelled.");
|
||||||
|
ShowResult(false);
|
||||||
|
}
|
||||||
|
catch (FonepayError e)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Fonepay API error {e.ErrorCode}: {e.Message}");
|
||||||
|
ShowResult(false);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException e)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Websocket closed early: {e.Message}");
|
||||||
|
ShowResult(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cts.Dispose();
|
||||||
|
if (_payCts == cts) _payCts = null;
|
||||||
|
if (this != null && payButton != null) payButton.interactable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowResult(bool ok)
|
||||||
|
{
|
||||||
|
if (qrImage != null)
|
||||||
|
{
|
||||||
|
qrImage.sprite = null;
|
||||||
|
qrImage.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
if (successObject != null) successObject.SetActive(ok);
|
||||||
|
if (failedObject != null) failedObject.SetActive(!ok);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d53a9483205b945d69e846f2285560bd
|
||||||
@@ -151,9 +151,9 @@ RectTransform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 508689485}
|
m_Father: {fileID: 508689485}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
m_AnchorMin: {x: 0.5, y: 1}
|
||||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
m_AnchorMax: {x: 0.5, y: 1}
|
||||||
m_AnchoredPosition: {x: 0, y: -140.79999}
|
m_AnchoredPosition: {x: 0, y: -69}
|
||||||
m_SizeDelta: {x: 200, y: 50}
|
m_SizeDelta: {x: 200, y: 50}
|
||||||
m_Pivot: {x: 0.5, y: 0.5}
|
m_Pivot: {x: 0.5, y: 0.5}
|
||||||
--- !u!114 &454749
|
--- !u!114 &454749
|
||||||
@@ -388,7 +388,8 @@ RectTransform:
|
|||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 7.9915, y: 7.9915, z: 7.9915}
|
m_LocalScale: {x: 7.9915, y: 7.9915, z: 7.9915}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children:
|
||||||
|
- {fileID: 1350158650}
|
||||||
m_Father: {fileID: 508689485}
|
m_Father: {fileID: 508689485}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||||
@@ -524,13 +525,15 @@ MonoBehaviour:
|
|||||||
m_GameObject: {fileID: 508689484}
|
m_GameObject: {fileID: 508689484}
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
m_EditorHideFlags: 0
|
m_EditorHideFlags: 0
|
||||||
m_Script: {fileID: 11500000, guid: 57da22305386447fcabd663378359ede, type: 3}
|
m_Script: {fileID: 11500000, guid: d53a9483205b945d69e846f2285560bd, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::Darkmatter.Fonepay.Samples.SamplePayment
|
m_EditorClassIdentifier: Assembly-CSharp::Darkmatter.Fonepay.Samples.SamplePayment
|
||||||
qrImage: {fileID: 308706331}
|
qrImage: {fileID: 308706331}
|
||||||
successObject: {fileID: 454747}
|
successObject: {fileID: 454747}
|
||||||
failedObject: {fileID: 1400840764}
|
failedObject: {fileID: 1400840764}
|
||||||
payButton: {fileID: 1002816889}
|
payButton: {fileID: 1002816889}
|
||||||
|
cancelButton: {fileID: 1350158652}
|
||||||
|
amount: 1
|
||||||
--- !u!1 &519420028
|
--- !u!1 &519420028
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -789,6 +792,140 @@ CanvasRenderer:
|
|||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1002816887}
|
m_GameObject: {fileID: 1002816887}
|
||||||
m_CullTransparentMesh: 1
|
m_CullTransparentMesh: 1
|
||||||
|
--- !u!1 &1350158649
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1350158650}
|
||||||
|
- component: {fileID: 1350158654}
|
||||||
|
- component: {fileID: 1350158653}
|
||||||
|
- component: {fileID: 1350158652}
|
||||||
|
- component: {fileID: 1350158651}
|
||||||
|
m_Layer: 5
|
||||||
|
m_Name: Cancel
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!224 &1350158650
|
||||||
|
RectTransform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1350158649}
|
||||||
|
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 0.18375774, y: 0.18375774, z: 0.18375774}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children:
|
||||||
|
- {fileID: 1884838835}
|
||||||
|
m_Father: {fileID: 308706330}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
m_AnchorMin: {x: 0.5, y: 0}
|
||||||
|
m_AnchorMax: {x: 0.5, y: 0}
|
||||||
|
m_AnchoredPosition: {x: 0, y: -12.8}
|
||||||
|
m_SizeDelta: {x: 160, y: 30}
|
||||||
|
m_Pivot: {x: 0.5, y: 0.5}
|
||||||
|
--- !u!114 &1350158651
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1350158649}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 7d55618be80a043b0b467209e1a80cbb, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::Darkmatter.Fonepay.Samples.SamplePayment
|
||||||
|
--- !u!114 &1350158652
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1350158649}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||||
|
m_Navigation:
|
||||||
|
m_Mode: 3
|
||||||
|
m_WrapAround: 0
|
||||||
|
m_SelectOnUp: {fileID: 0}
|
||||||
|
m_SelectOnDown: {fileID: 0}
|
||||||
|
m_SelectOnLeft: {fileID: 0}
|
||||||
|
m_SelectOnRight: {fileID: 0}
|
||||||
|
m_Transition: 1
|
||||||
|
m_Colors:
|
||||||
|
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||||
|
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||||
|
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||||
|
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||||
|
m_ColorMultiplier: 1
|
||||||
|
m_FadeDuration: 0.1
|
||||||
|
m_SpriteState:
|
||||||
|
m_HighlightedSprite: {fileID: 0}
|
||||||
|
m_PressedSprite: {fileID: 0}
|
||||||
|
m_SelectedSprite: {fileID: 0}
|
||||||
|
m_DisabledSprite: {fileID: 0}
|
||||||
|
m_AnimationTriggers:
|
||||||
|
m_NormalTrigger: Normal
|
||||||
|
m_HighlightedTrigger: Highlighted
|
||||||
|
m_PressedTrigger: Pressed
|
||||||
|
m_SelectedTrigger: Selected
|
||||||
|
m_DisabledTrigger: Disabled
|
||||||
|
m_Interactable: 1
|
||||||
|
m_TargetGraphic: {fileID: 1350158653}
|
||||||
|
m_OnClick:
|
||||||
|
m_PersistentCalls:
|
||||||
|
m_Calls: []
|
||||||
|
--- !u!114 &1350158653
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1350158649}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
m_RaycastTarget: 1
|
||||||
|
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
m_Maskable: 1
|
||||||
|
m_OnCullStateChanged:
|
||||||
|
m_PersistentCalls:
|
||||||
|
m_Calls: []
|
||||||
|
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_Type: 1
|
||||||
|
m_PreserveAspect: 0
|
||||||
|
m_FillCenter: 1
|
||||||
|
m_FillMethod: 4
|
||||||
|
m_FillAmount: 1
|
||||||
|
m_FillClockwise: 1
|
||||||
|
m_FillOrigin: 0
|
||||||
|
m_UseSpriteMesh: 0
|
||||||
|
m_PixelsPerUnitMultiplier: 1
|
||||||
|
--- !u!222 &1350158654
|
||||||
|
CanvasRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1350158649}
|
||||||
|
m_CullTransparentMesh: 1
|
||||||
--- !u!1 &1400840764
|
--- !u!1 &1400840764
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -821,9 +958,9 @@ RectTransform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 508689485}
|
m_Father: {fileID: 508689485}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
m_AnchorMin: {x: 0.5, y: 1}
|
||||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
m_AnchorMax: {x: 0.5, y: 1}
|
||||||
m_AnchoredPosition: {x: 0, y: -140.8}
|
m_AnchoredPosition: {x: 0, y: -69}
|
||||||
m_SizeDelta: {x: 200, y: 50}
|
m_SizeDelta: {x: 200, y: 50}
|
||||||
m_Pivot: {x: 0.5, y: 0.5}
|
m_Pivot: {x: 0.5, y: 0.5}
|
||||||
--- !u!114 &1400840766
|
--- !u!114 &1400840766
|
||||||
@@ -1063,6 +1200,143 @@ CanvasRenderer:
|
|||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 1625398629}
|
m_GameObject: {fileID: 1625398629}
|
||||||
m_CullTransparentMesh: 1
|
m_CullTransparentMesh: 1
|
||||||
|
--- !u!1 &1884838834
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1884838835}
|
||||||
|
- component: {fileID: 1884838837}
|
||||||
|
- component: {fileID: 1884838836}
|
||||||
|
m_Layer: 5
|
||||||
|
m_Name: Text (TMP)
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!224 &1884838835
|
||||||
|
RectTransform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1884838834}
|
||||||
|
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 1350158650}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
|
m_AnchorMax: {x: 1, y: 1}
|
||||||
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
|
m_SizeDelta: {x: 0, y: 0}
|
||||||
|
m_Pivot: {x: 0.5, y: 0.5}
|
||||||
|
--- !u!114 &1884838836
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1884838834}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
m_RaycastTarget: 1
|
||||||
|
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
m_Maskable: 1
|
||||||
|
m_OnCullStateChanged:
|
||||||
|
m_PersistentCalls:
|
||||||
|
m_Calls: []
|
||||||
|
m_text: Cancel
|
||||||
|
m_isRightToLeft: 0
|
||||||
|
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
|
||||||
|
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
|
||||||
|
m_fontSharedMaterials: []
|
||||||
|
m_fontMaterial: {fileID: 0}
|
||||||
|
m_fontMaterials: []
|
||||||
|
m_fontColor32:
|
||||||
|
serializedVersion: 2
|
||||||
|
rgba: 4281479730
|
||||||
|
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||||
|
m_enableVertexGradient: 0
|
||||||
|
m_colorMode: 3
|
||||||
|
m_fontColorGradient:
|
||||||
|
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
m_fontColorGradientPreset: {fileID: 0}
|
||||||
|
m_spriteAsset: {fileID: 0}
|
||||||
|
m_tintAllSprites: 0
|
||||||
|
m_StyleSheet: {fileID: 0}
|
||||||
|
m_TextStyleHashCode: -1183493901
|
||||||
|
m_overrideHtmlColors: 0
|
||||||
|
m_faceColor:
|
||||||
|
serializedVersion: 2
|
||||||
|
rgba: 4294967295
|
||||||
|
m_fontSize: 24
|
||||||
|
m_fontSizeBase: 24
|
||||||
|
m_fontWeight: 400
|
||||||
|
m_enableAutoSizing: 0
|
||||||
|
m_fontSizeMin: 18
|
||||||
|
m_fontSizeMax: 72
|
||||||
|
m_fontStyle: 0
|
||||||
|
m_HorizontalAlignment: 2
|
||||||
|
m_VerticalAlignment: 512
|
||||||
|
m_textAlignment: 65535
|
||||||
|
m_characterSpacing: 0
|
||||||
|
m_characterHorizontalScale: 1
|
||||||
|
m_wordSpacing: 0
|
||||||
|
m_lineSpacing: 0
|
||||||
|
m_lineSpacingMax: 0
|
||||||
|
m_paragraphSpacing: 0
|
||||||
|
m_charWidthMaxAdj: 0
|
||||||
|
m_TextWrappingMode: 1
|
||||||
|
m_wordWrappingRatios: 0.4
|
||||||
|
m_overflowMode: 0
|
||||||
|
m_linkedTextComponent: {fileID: 0}
|
||||||
|
parentLinkedComponent: {fileID: 0}
|
||||||
|
m_enableKerning: 0
|
||||||
|
m_ActiveFontFeatures: 6e72656b
|
||||||
|
m_enableExtraPadding: 0
|
||||||
|
checkPaddingRequired: 0
|
||||||
|
m_isRichText: 1
|
||||||
|
m_EmojiFallbackSupport: 1
|
||||||
|
m_parseCtrlCharacters: 1
|
||||||
|
m_isOrthographic: 1
|
||||||
|
m_isCullingEnabled: 0
|
||||||
|
m_horizontalMapping: 0
|
||||||
|
m_verticalMapping: 0
|
||||||
|
m_uvLineOffset: 0
|
||||||
|
m_geometrySortingOrder: 0
|
||||||
|
m_IsTextObjectScaleStatic: 0
|
||||||
|
m_VertexBufferAutoSizeReduction: 0
|
||||||
|
m_useMaxVisibleDescender: 1
|
||||||
|
m_pageToDisplay: 1
|
||||||
|
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
m_isUsingLegacyAnimationComponent: 0
|
||||||
|
m_isVolumetricText: 0
|
||||||
|
m_hasFontAssetChanged: 0
|
||||||
|
m_baseMaterial: {fileID: 0}
|
||||||
|
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
--- !u!222 &1884838837
|
||||||
|
CanvasRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1884838834}
|
||||||
|
m_CullTransparentMesh: 1
|
||||||
--- !u!1 &1949324813
|
--- !u!1 &1949324813
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
BIN
Packages/.DS_Store
vendored
Normal file
BIN
Packages/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Packages/com.darkmattergameproduction.fonepay-unity/.DS_Store
vendored
Normal file
BIN
Packages/com.darkmattergameproduction.fonepay-unity/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,24 @@
|
|||||||
|
# Changelog
|
||||||
|
All notable changes to this package will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-05-07
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- All `FonepayClient` public methods now return `FonepayAsync<T>` / `FonepayAsync` instead of `Task<T>` / `Task`. Callers `await` the same way; UniTask users get native `UniTask` under `#if UNITASK_SUPPORT`.
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-05-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `FonepayClient` façade: `PurchaseAsync`, `GetStatusAsync`, `AwaitPaymentAsync`, `PostTaxRefundAsync`.
|
||||||
|
- HMAC-SHA512 signing of all signed payloads.
|
||||||
|
- Editor tooling for credential management (Tools > Fonepay > Settings).
|
||||||
|
- Example sample under Package Manager > Samples.
|
||||||
|
- Edit-mode tests covering `WebsocketMessage<T>.Status` parsing and `PaymentOutcome` rules.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `WebsocketMessage<T>.Status` returned default values due to invalid `??=` cache on generic struct. Now reparses on each access.
|
||||||
|
- `AwaitPaymentAsync` hung indefinitely when the server closed the websocket before a payment frame; now throws `InvalidOperationException`.
|
||||||
|
- `QRPaymentStatus.transactionDate` changed from `DateTime` to `string` (JsonUtility cannot deserialize `DateTime`).
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Fonepay Unity
|
||||||
|
|
||||||
|
See [README](../README.md) for setup, API reference, and examples.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
| Layer | Type | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| Public | `FonepayClient` | Façade. Auto-loads config + secrets. |
|
||||||
|
| API | `FonepayApiClient` | Signed REST calls (QR, status, tax refund). |
|
||||||
|
| Websocket | `FonepayWebsocketClient` | Receive-loop for QR verification + payment frames. |
|
||||||
|
| Crypto | `HmacSha512Signer` | HMAC-SHA512 dataValidation. |
|
||||||
|
| Models | `QrRequest`/`QrResult`/`QRPaymentStatus`/... | Serializable DTOs. |
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
PurchaseAsync(req) ── POST QR ──▶ QrResult { qrCode, thirdpartyQrWebSocketUrl }
|
||||||
|
AwaitPaymentAsync(url) ── WS ──▶ QRPaymentStatus { Outcome }
|
||||||
|
├─ qrVerified frame → onQrVerified callback
|
||||||
|
└─ paymentSuccess frame → resolve
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outcome rules
|
||||||
|
|
||||||
|
| `success` | `paymentSuccess` | `Outcome` |
|
||||||
|
|---|---|---|
|
||||||
|
| true | true | Complete |
|
||||||
|
| true | false | CancelledByUser |
|
||||||
|
| false | * | Failed |
|
||||||
|
|
||||||
|
## Termination
|
||||||
|
|
||||||
|
- Payment frame received → resolve normally.
|
||||||
|
- `CancellationToken` cancelled → `OperationCanceledException`, socket disconnects.
|
||||||
|
- Server closes socket before payment → `InvalidOperationException`.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Fonepay Unity
|
||||||
|
|
||||||
|
Fonepay payment integration for Unity. Request QR codes, await payment confirmation over websocket, process tax refunds — all from a single async-friendly client.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Unity 6000.4+
|
||||||
|
- [UniTask](https://github.com/Cysharp/UniTask) (for sample only — runtime uses `System.Threading.Tasks`)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Install the package via Package Manager (Git URL or local).
|
||||||
|
2. Open **Tools > Fonepay > Settings** to create the `FonepayConfig` asset and enter your merchant credentials. Credentials are kept out of source control and baked at build time.
|
||||||
|
3. (Optional) **Window > Package Manager > Fonepay Unity > Samples > Import** to drop the example MonoBehaviour into your project.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### 1. Request a QR
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Darkmatter.Fonepay;
|
||||||
|
|
||||||
|
var fonepay = new FonepayClient();
|
||||||
|
|
||||||
|
QrResult qr = await fonepay.PurchaseAsync(new QrRequest
|
||||||
|
{
|
||||||
|
amount = 100f,
|
||||||
|
remarks1 = "order #1234"
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
// qr.qrCode is a Texture2D ready to render
|
||||||
|
// qr.thirdpartyQrWebSocketUrl — pass to AwaitPaymentAsync
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Await payment
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
QRPaymentStatus result = await fonepay.AwaitPaymentAsync(
|
||||||
|
qr.thirdpartyQrWebSocketUrl,
|
||||||
|
onQrVerified: verified => Debug.Log($"QR scanned: {verified}"),
|
||||||
|
ct: ct);
|
||||||
|
|
||||||
|
switch (result.Outcome)
|
||||||
|
{
|
||||||
|
case PaymentOutcome.Complete: // success
|
||||||
|
case PaymentOutcome.CancelledByUser: // user dismissed in app
|
||||||
|
case PaymentOutcome.Failed: // server rejected
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`AwaitPaymentAsync` opens the websocket, fires `onQrVerified` when the QR is scanned, then resolves on the terminal payment frame and disconnects.
|
||||||
|
|
||||||
|
### 3. Cancellation & timeout
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var cts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
cts.CancelAfter(TimeSpan.FromMinutes(15)); // QR validity
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var payment = await fonepay.AwaitPaymentAsync(qr.thirdpartyQrWebSocketUrl, ct: cts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { /* user cancelled or timed out */ }
|
||||||
|
catch (InvalidOperationException) { /* server closed websocket early */ }
|
||||||
|
catch (FonepayError e) { /* API error — e.ErrorCode, e.Docs */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
Calling `cts.Cancel()` from a button handler aborts the await and disconnects the socket.
|
||||||
|
|
||||||
|
### 4. Tax refund
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
TaxRefundResponse refund = await fonepay.PostTaxRefundAsync(new TaxRefundRequest
|
||||||
|
{
|
||||||
|
fonepayTraceId = "...",
|
||||||
|
merchantPRN = "...",
|
||||||
|
invoiceNumber = "INV-001",
|
||||||
|
invoiceDate = DateTime.UtcNow,
|
||||||
|
transactionAmount = 100f,
|
||||||
|
}, ct);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
`FonepayError` (extends `Exception`) carries `ErrorCode` and `Docs`. Throw paths:
|
||||||
|
- Missing/invalid `FonepayConfig` asset or credentials
|
||||||
|
- Non-2xx HTTP responses from Fonepay
|
||||||
|
- HMAC signing failures
|
||||||
|
|
||||||
|
## Samples
|
||||||
|
|
||||||
|
Import **Example Payment Flow** from the Package Manager. Wires a button → QR image → success/fail panels with cancel support.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Editor tests live under `Tests/Editor` (NUnit). Run via **Window > General > Test Runner > EditMode**.
|
||||||
@@ -177,14 +177,29 @@ namespace Darkmatter.Fonepay
|
|||||||
return new QrResult
|
return new QrResult
|
||||||
{
|
{
|
||||||
message = response.message,
|
message = response.message,
|
||||||
qrCode = string.IsNullOrEmpty(response.qrMessage)
|
qrCode = TryGenerateQr(response.qrMessage),
|
||||||
? null
|
|
||||||
: FonepayQRGenerator.GenerateTexture(response.qrMessage),
|
|
||||||
status = response.status,
|
status = response.status,
|
||||||
statusCode = response.statusCode,
|
statusCode = response.statusCode,
|
||||||
success = response.success,
|
success = response.success,
|
||||||
thirdpartyQrWebSocketUrl = response.thirdpartyQrWebSocketUrl,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ namespace Darkmatter.Fonepay
|
|||||||
internal event Action<bool> OnQrVerified;
|
internal event Action<bool> OnQrVerified;
|
||||||
internal event Action<WebsocketMessage<QRPaymentStatus>> OnPaymentReceived;
|
internal event Action<WebsocketMessage<QRPaymentStatus>> OnPaymentReceived;
|
||||||
internal event Action<string> OnRawMessage;
|
internal event Action<string> OnRawMessage;
|
||||||
|
internal event Action<Exception> OnClosed;
|
||||||
|
|
||||||
private ClientWebSocket _client;
|
private ClientWebSocket _client;
|
||||||
private CancellationTokenSource _cts;
|
private CancellationTokenSource _cts;
|
||||||
@@ -51,6 +52,7 @@ namespace Darkmatter.Fonepay
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var buffer = new byte[4096];
|
var buffer = new byte[4096];
|
||||||
|
Exception error = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -70,9 +72,9 @@ namespace Darkmatter.Fonepay
|
|||||||
{
|
{
|
||||||
// Expected during disconnect.
|
// Expected during disconnect.
|
||||||
}
|
}
|
||||||
catch (WebSocketException)
|
catch (WebSocketException ex)
|
||||||
{
|
{
|
||||||
// Network disconnect or broken socket.
|
error = ex;
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
@@ -80,7 +82,11 @@ namespace Darkmatter.Fonepay
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"WebSocket receive error: {ex.Message}");
|
error = ex;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
OnClosed?.Invoke(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Darkmatter.Fonepay
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FonepayAsync<QrResult> PurchaseAsync(QrRequest req, CancellationToken ct = default)
|
||||||
|
=> FonepayAsyncBridge.Wrap(_api.PostQRAsync(req, ct), ct);
|
||||||
|
|
||||||
|
public FonepayAsync<QrResult> GetStatusAsync(string prn, CancellationToken ct = default)
|
||||||
|
=> FonepayAsyncBridge.Wrap(_api.GetStatusAsync(prn, ct), ct);
|
||||||
|
|
||||||
|
public FonepayAsync<TaxRefundResponse> PostTaxRefundAsync(TaxRefundRequest req, CancellationToken ct = default)
|
||||||
|
=> FonepayAsyncBridge.Wrap(_api.PostTaxRefundAsync(req, ct), ct);
|
||||||
|
|
||||||
|
public FonepayAsync<QRPaymentStatus> AwaitPaymentAsync(
|
||||||
|
string websocketUrl,
|
||||||
|
Action<bool> onQrVerified = null,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
=> FonepayAsyncBridge.Wrap(AwaitPaymentInternalAsync(websocketUrl, onQrVerified, ct), ct);
|
||||||
|
|
||||||
|
private async Task<QRPaymentStatus> AwaitPaymentInternalAsync(
|
||||||
|
string websocketUrl,
|
||||||
|
Action<bool> onQrVerified,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(websocketUrl))
|
||||||
|
throw new ArgumentException("Websocket URL required", nameof(websocketUrl));
|
||||||
|
|
||||||
|
using var ws = new FonepayWebsocketClient();
|
||||||
|
var tcs = new TaskCompletionSource<QRPaymentStatus>(
|
||||||
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
if (onQrVerified != null)
|
||||||
|
ws.OnQrVerified += onQrVerified;
|
||||||
|
|
||||||
|
ws.OnPaymentReceived += msg => tcs.TrySetResult(msg.Status);
|
||||||
|
ws.OnClosed += err =>
|
||||||
|
{
|
||||||
|
if (err != null)
|
||||||
|
tcs.TrySetException(err);
|
||||||
|
else
|
||||||
|
tcs.TrySetException(new InvalidOperationException(
|
||||||
|
"Fonepay websocket closed before payment frame received."));
|
||||||
|
};
|
||||||
|
|
||||||
|
using var ctReg = ct.Register(() => tcs.TrySetCanceled(ct));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ws.ConnectAsync(websocketUrl, ct);
|
||||||
|
return await tcs.Task;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await ws.DisconnectAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ namespace Darkmatter.Fonepay
|
|||||||
public int statusCode;
|
public int statusCode;
|
||||||
public bool success;
|
public bool success;
|
||||||
public string thirdpartyQrWebSocketUrl;
|
public string thirdpartyQrWebSocketUrl;
|
||||||
|
public string qrMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ namespace Darkmatter.Fonepay
|
|||||||
{
|
{
|
||||||
public string remarks1;
|
public string remarks1;
|
||||||
public string remarks2;
|
public string remarks2;
|
||||||
public DateTime transactionDate;
|
public string transactionDate;
|
||||||
public string productNumber;
|
public string productNumber;
|
||||||
public float amount;
|
public float amount;
|
||||||
public string message;
|
public string message;
|
||||||
@@ -9,7 +9,6 @@ namespace Darkmatter.Fonepay
|
|||||||
public string merchantId;
|
public string merchantId;
|
||||||
public string deviceId;
|
public string deviceId;
|
||||||
public string transactionStatus;
|
public string transactionStatus;
|
||||||
private T _status;
|
public T Status => JsonUtility.FromJson<T>(transactionStatus);
|
||||||
public T Status => _status ??= JsonUtility.FromJson<T>(transactionStatus);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: debdfefd8b01242e8b95f0e29bb09c0d
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3902a28335b7043bf831aab193145f4c
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
@@ -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:
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using QRCoder;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace Darkmatter.Fonepay
|
namespace Darkmatter.Fonepay
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pure C# QR Code generator. Byte mode, ECC L/M/Q/H, versions 1–10.
|
/// QR code generator backed by QRCoder (MIT). See Plugins/QRCoder-LICENSE.txt.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class FonepayQRGenerator
|
public static class FonepayQRGenerator
|
||||||
{
|
{
|
||||||
public enum EccLevel { L, M, Q, H }
|
public enum EccLevel { L, M, Q, H }
|
||||||
|
|
||||||
@@ -25,16 +27,21 @@ namespace Darkmatter.Fonepay
|
|||||||
Color dark = darkColor ?? Color.black;
|
Color dark = darkColor ?? Color.black;
|
||||||
Color light = lightColor ?? Color.white;
|
Color light = lightColor ?? Color.white;
|
||||||
|
|
||||||
|
var pixels = new Color[texSize * texSize];
|
||||||
for (int row = 0; row < size; row++)
|
for (int row = 0; row < size; row++)
|
||||||
for (int col = 0; col < size; col++)
|
for (int col = 0; col < size; col++)
|
||||||
{
|
{
|
||||||
Color c = matrix[row, col] ? dark : light;
|
Color c = matrix[row, col] ? dark : light;
|
||||||
|
int yBase = (size - 1 - row) * pixelSize;
|
||||||
|
int xBase = col * pixelSize;
|
||||||
for (int py = 0; py < pixelSize; py++)
|
for (int py = 0; py < pixelSize; py++)
|
||||||
|
{
|
||||||
|
int rowStart = (yBase + py) * texSize + xBase;
|
||||||
for (int px = 0; px < pixelSize; px++)
|
for (int px = 0; px < pixelSize; px++)
|
||||||
tex.SetPixel(col * pixelSize + px,
|
pixels[rowStart + px] = c;
|
||||||
(size - 1 - row) * pixelSize + py, c);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
tex.SetPixels(pixels);
|
||||||
tex.Apply();
|
tex.Apply();
|
||||||
return tex;
|
return tex;
|
||||||
}
|
}
|
||||||
@@ -57,10 +64,26 @@ namespace Darkmatter.Fonepay
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text))
|
if (string.IsNullOrEmpty(text))
|
||||||
throw new System.ArgumentException("QR text must be non-empty", nameof(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);
|
using var data = new QRCodeGenerator().CreateQrCode(text, MapEcc(ecc), forceUtf8: true);
|
||||||
byte[] codewords = BuildCodewords(data, version, ecc, ecBlocks);
|
int size = data.ModuleMatrix.Count;
|
||||||
return BuildMatrix(version, codewords);
|
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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
Packages/com.darkmattergameproduction.fonepay-unity/Samples~/.DS_Store
vendored
Normal file
BIN
Packages/com.darkmattergameproduction.fonepay-unity/Samples~/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: a65b4dbd6e610485fae19b0acddf3890
|
guid: 65d000fc5ca9742ec882f227d68c7bd4
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
BIN
Packages/com.darkmattergameproduction.fonepay-unity/Samples~/Example/.DS_Store
vendored
Normal file
BIN
Packages/com.darkmattergameproduction.fonepay-unity/Samples~/Example/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"displayName":"Example Sample",
|
||||||
|
"description": "Replace this string with your own description of the sample. Delete the Samples folder if not needed."
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace Darkmatter.Fonepay.Samples
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal end-to-end sample: request QR, render to Image, await payment,
|
||||||
|
/// support cancellation via a cancel button.
|
||||||
|
/// </summary>
|
||||||
|
public class SamplePayment : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private Image qrImage;
|
||||||
|
[SerializeField] private GameObject successObject;
|
||||||
|
[SerializeField] private GameObject failedObject;
|
||||||
|
[SerializeField] private Button payButton;
|
||||||
|
[SerializeField] private Button cancelButton;
|
||||||
|
[SerializeField] private float amount = 1f;
|
||||||
|
|
||||||
|
private CancellationTokenSource _payCts;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
payButton.onClick.AddListener(() => InitiatePayment().Forget());
|
||||||
|
if (cancelButton != null)
|
||||||
|
cancelButton.onClick.AddListener(() => _payCts?.Cancel());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UniTask InitiatePayment()
|
||||||
|
{
|
||||||
|
if (_payCts != null) return;
|
||||||
|
|
||||||
|
payButton.interactable = false;
|
||||||
|
var fonepay = new FonepayClient();
|
||||||
|
var cts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
_payCts = cts;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var qr = await fonepay.PurchaseAsync(
|
||||||
|
new QrRequest { amount = amount, remarks1 = "sample" },
|
||||||
|
cts.Token);
|
||||||
|
|
||||||
|
if (qr.qrCode != null)
|
||||||
|
{
|
||||||
|
qrImage.sprite = Sprite.Create(
|
||||||
|
qr.qrCode,
|
||||||
|
new Rect(0, 0, qr.qrCode.width, qr.qrCode.height),
|
||||||
|
new Vector2(0.5f, 0.5f));
|
||||||
|
qrImage.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var payment = await fonepay.AwaitPaymentAsync(
|
||||||
|
qr.thirdpartyQrWebSocketUrl,
|
||||||
|
onQrVerified: v => Debug.Log($"Fonepay QR verified: {v}"),
|
||||||
|
ct: cts.Token);
|
||||||
|
|
||||||
|
Debug.Log($"Payment frame: {JsonUtility.ToJson(payment)}");
|
||||||
|
ShowResult(payment.Outcome == PaymentOutcome.Complete);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Debug.Log("Payment cancelled.");
|
||||||
|
ShowResult(false);
|
||||||
|
}
|
||||||
|
catch (FonepayError e)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Fonepay API error {e.ErrorCode}: {e.Message}");
|
||||||
|
ShowResult(false);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException e)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Websocket closed early: {e.Message}");
|
||||||
|
ShowResult(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cts.Dispose();
|
||||||
|
if (_payCts == cts) _payCts = null;
|
||||||
|
if (this != null && payButton != null) payButton.interactable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowResult(bool ok)
|
||||||
|
{
|
||||||
|
if (qrImage != null)
|
||||||
|
{
|
||||||
|
qrImage.sprite = null;
|
||||||
|
qrImage.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
if (successObject != null) successObject.SetActive(ok);
|
||||||
|
if (failedObject != null) failedObject.SetActive(!ok);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d53a9483205b945d69e846f2285560bd
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user