Compare commits
7 Commits
6a8a6e46f0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b609c43199 | ||
|
|
ac61fdba83 | ||
|
|
98526d82d9 | ||
|
|
f7f287bca7 | ||
|
|
5515a88cf5 | ||
|
|
028a1a7f46 | ||
|
|
dface29ff2 |
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*
|
||||||
8
Assets/Samples.meta
Normal file
8
Assets/Samples.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5dff20c6950e54d71bfe971186191b01
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Samples/Fonepay Unity.meta
Normal file
8
Assets/Samples/Fonepay Unity.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2b7e8dee7532643a181e55ce459fef09
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
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,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
|
||||||
@@ -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}
|
||||||
@@ -445,6 +446,7 @@ GameObject:
|
|||||||
- component: {fileID: 508689485}
|
- component: {fileID: 508689485}
|
||||||
- component: {fileID: 508689487}
|
- component: {fileID: 508689487}
|
||||||
- component: {fileID: 508689486}
|
- component: {fileID: 508689486}
|
||||||
|
- component: {fileID: 508689488}
|
||||||
m_Layer: 5
|
m_Layer: 5
|
||||||
m_Name: Image
|
m_Name: Image
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -514,6 +516,24 @@ CanvasRenderer:
|
|||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 508689484}
|
m_GameObject: {fileID: 508689484}
|
||||||
m_CullTransparentMesh: 1
|
m_CullTransparentMesh: 1
|
||||||
|
--- !u!114 &508689488
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 508689484}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: d53a9483205b945d69e846f2285560bd, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::Darkmatter.Fonepay.Samples.SamplePayment
|
||||||
|
qrImage: {fileID: 308706331}
|
||||||
|
successObject: {fileID: 454747}
|
||||||
|
failedObject: {fileID: 1400840764}
|
||||||
|
payButton: {fileID: 1002816889}
|
||||||
|
cancelButton: {fileID: 1350158652}
|
||||||
|
amount: 1
|
||||||
--- !u!1 &519420028
|
--- !u!1 &519420028
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -772,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
|
||||||
@@ -804,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
|
||||||
@@ -1046,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.
@@ -4,6 +4,11 @@ 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/)
|
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).
|
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
|
## [0.1.0] - 2026-05-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Darkmatter.Fonepay
|
namespace Darkmatter.Fonepay
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Public facade. Auto-loads config + secrets via <see cref="FonepayConfig.Load"/>.
|
|
||||||
/// Caller never touches credentials.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FonepayClient
|
public sealed class FonepayClient
|
||||||
{
|
{
|
||||||
private readonly FonepayApiClient _api;
|
private readonly FonepayApiClient _api;
|
||||||
@@ -22,41 +18,25 @@ namespace Darkmatter.Fonepay
|
|||||||
_api = new FonepayApiClient(config, signer);
|
_api = new FonepayApiClient(config, signer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public FonepayAsync<QrResult> PurchaseAsync(QrRequest req, CancellationToken ct = default)
|
||||||
/// Request a new QR code. The returned URL is valid for 15 minutes. Call GetStatusAsync() to check if the QR code has been paid.
|
=> FonepayAsyncBridge.Wrap(_api.PostQRAsync(req, ct), ct);
|
||||||
/// </summary>
|
|
||||||
/// <param name="req"></param>
|
|
||||||
/// <param name="ct"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<QrResult> PurchaseAsync(QrRequest req, CancellationToken ct = default)
|
|
||||||
=> _api.PostQRAsync(req, ct);
|
|
||||||
|
|
||||||
/// <summary>
|
public FonepayAsync<QrResult> GetStatusAsync(string prn, CancellationToken ct = default)
|
||||||
/// 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.
|
=> FonepayAsyncBridge.Wrap(_api.GetStatusAsync(prn, ct), ct);
|
||||||
/// </summary>
|
|
||||||
/// <param name="prn"></param>
|
|
||||||
/// <param name="ct"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<QrResult> GetStatusAsync(string prn, CancellationToken ct = default)
|
|
||||||
=> _api.GetStatusAsync(prn, ct);
|
|
||||||
|
|
||||||
/// <summary>
|
public FonepayAsync<TaxRefundResponse> PostTaxRefundAsync(TaxRefundRequest req, CancellationToken ct = default)
|
||||||
/// Request a tax refund. Returns "REFUND_SUCCESS" if successful, or an error message if the PRN is invalid, expired, or not eligible for refund.
|
=> FonepayAsyncBridge.Wrap(_api.PostTaxRefundAsync(req, ct), ct);
|
||||||
/// </summary>
|
|
||||||
/// <param name="req"></param>
|
|
||||||
/// <param name="ct"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task<TaxRefundResponse> PostTaxRefundAsync(TaxRefundRequest req, CancellationToken ct = default)
|
|
||||||
=> _api.PostTaxRefundAsync(req, ct);
|
|
||||||
|
|
||||||
/// <summary>
|
public FonepayAsync<QRPaymentStatus> AwaitPaymentAsync(
|
||||||
/// Connects to the QR websocket and awaits the terminal payment frame, then auto-disconnects.
|
|
||||||
/// Pass <see cref="QrResult.thirdpartyQrWebSocketUrl"/> from <see cref="PurchaseAsync"/>.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<QRPaymentStatus> AwaitPaymentAsync(
|
|
||||||
string websocketUrl,
|
string websocketUrl,
|
||||||
Action<bool> onQrVerified = null,
|
Action<bool> onQrVerified = null,
|
||||||
CancellationToken ct = default)
|
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))
|
if (string.IsNullOrEmpty(websocketUrl))
|
||||||
throw new ArgumentException("Websocket URL required", nameof(websocketUrl));
|
throw new ArgumentException("Websocket URL required", nameof(websocketUrl));
|
||||||
|
|||||||
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.
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 65d000fc5ca9742ec882f227d68c7bd4
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
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.
@@ -30,14 +30,18 @@ namespace Darkmatter.Fonepay.Samples
|
|||||||
|
|
||||||
private async UniTask InitiatePayment()
|
private async UniTask InitiatePayment()
|
||||||
{
|
{
|
||||||
|
if (_payCts != null) return;
|
||||||
|
|
||||||
|
payButton.interactable = false;
|
||||||
var fonepay = new FonepayClient();
|
var fonepay = new FonepayClient();
|
||||||
_payCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
var cts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
_payCts = cts;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var qr = await fonepay.PurchaseAsync(
|
var qr = await fonepay.PurchaseAsync(
|
||||||
new QrRequest { amount = amount, remarks1 = "sample" },
|
new QrRequest { amount = amount, remarks1 = "sample" },
|
||||||
_payCts.Token);
|
cts.Token);
|
||||||
|
|
||||||
if (qr.qrCode != null)
|
if (qr.qrCode != null)
|
||||||
{
|
{
|
||||||
@@ -51,7 +55,7 @@ namespace Darkmatter.Fonepay.Samples
|
|||||||
var payment = await fonepay.AwaitPaymentAsync(
|
var payment = await fonepay.AwaitPaymentAsync(
|
||||||
qr.thirdpartyQrWebSocketUrl,
|
qr.thirdpartyQrWebSocketUrl,
|
||||||
onQrVerified: v => Debug.Log($"Fonepay QR verified: {v}"),
|
onQrVerified: v => Debug.Log($"Fonepay QR verified: {v}"),
|
||||||
ct: _payCts.Token);
|
ct: cts.Token);
|
||||||
|
|
||||||
Debug.Log($"Payment frame: {JsonUtility.ToJson(payment)}");
|
Debug.Log($"Payment frame: {JsonUtility.ToJson(payment)}");
|
||||||
ShowResult(payment.Outcome == PaymentOutcome.Complete);
|
ShowResult(payment.Outcome == PaymentOutcome.Complete);
|
||||||
@@ -73,14 +77,19 @@ namespace Darkmatter.Fonepay.Samples
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_payCts.Dispose();
|
cts.Dispose();
|
||||||
_payCts = null;
|
if (_payCts == cts) _payCts = null;
|
||||||
|
if (this != null && payButton != null) payButton.interactable = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowResult(bool ok)
|
private void ShowResult(bool ok)
|
||||||
{
|
{
|
||||||
|
if (qrImage != null)
|
||||||
|
{
|
||||||
|
qrImage.sprite = null;
|
||||||
qrImage.gameObject.SetActive(false);
|
qrImage.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
if (successObject != null) successObject.SetActive(ok);
|
if (successObject != null) successObject.SetActive(ok);
|
||||||
if (failedObject != null) failedObject.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
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8c9cfa26abfee488c85f1582747f6a02
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"rootNamespace": "",
|
"rootNamespace": "",
|
||||||
"references": [
|
"references": [
|
||||||
"Darkmatter.FonepayUnity",
|
"Darkmatter.FonepayUnity",
|
||||||
|
"UniTask",
|
||||||
"UnityEngine.TestRunner"
|
"UnityEngine.TestRunner"
|
||||||
],
|
],
|
||||||
"includePlatforms": [],
|
"includePlatforms": [],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "com.darkmattergameproduction.fonepay-unity",
|
"name": "com.darkmattergameproduction.fonepay-unity",
|
||||||
"displayName": "Fonepay Unity",
|
"displayName": "Fonepay Unity",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"unity": "6000.4",
|
"unity": "6000.4",
|
||||||
"unityRelease": "5f1",
|
"unityRelease": "5f1",
|
||||||
"description": "Fonepay payment integration for Unity. Generate Fonepay QR codes, await payment confirmation via websocket, and process tax refunds. Credentials managed via Tools > Fonepay > Settings.",
|
"description": "Fonepay payment integration for Unity. Generate Fonepay QR codes, await payment confirmation via websocket, and process tax refunds. Credentials managed via Tools > Fonepay > Settings.",
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Darkmatter.Fonepay;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.TestTools;
|
|
||||||
|
|
||||||
namespace Darkmatter.FonepayUnity.Tests
|
|
||||||
{
|
|
||||||
public class WebsocketMessagePlayModeTests
|
|
||||||
{
|
|
||||||
[Test]
|
|
||||||
public void Status_ParsesNestedJsonInPlayer()
|
|
||||||
{
|
|
||||||
const string raw =
|
|
||||||
"{\"transactionStatus\":\"{\\\"paymentSuccess\\\":true,\\\"success\\\":true,\\\"amount\\\":7}\"}";
|
|
||||||
|
|
||||||
var envelope = JsonUtility.FromJson<WebsocketMessage<QRPaymentStatus>>(raw);
|
|
||||||
var status = envelope.Status;
|
|
||||||
|
|
||||||
Assert.IsTrue(status.paymentSuccess);
|
|
||||||
Assert.AreEqual(7f, status.amount);
|
|
||||||
Assert.AreEqual(PaymentOutcome.Complete, status.Outcome);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Status_ReparsesEachAccess()
|
|
||||||
{
|
|
||||||
const string raw =
|
|
||||||
"{\"transactionStatus\":\"{\\\"success\\\":true,\\\"paymentSuccess\\\":true}\"}";
|
|
||||||
|
|
||||||
var envelope = JsonUtility.FromJson<WebsocketMessage<QRPaymentStatus>>(raw);
|
|
||||||
var a = envelope.Status;
|
|
||||||
var b = envelope.Status;
|
|
||||||
|
|
||||||
Assert.AreEqual(a.Outcome, b.Outcome);
|
|
||||||
Assert.AreEqual(PaymentOutcome.Complete, a.Outcome);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class FonepayClientPlayModeTests
|
|
||||||
{
|
|
||||||
[SetUp]
|
|
||||||
public void Reset() => FonepayConfig.Invalidate();
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Ctor_WithoutConfigAsset_Throws()
|
|
||||||
{
|
|
||||||
// No FonepayConfig resource in test context → Load() throws FonepayError.
|
|
||||||
Assert.Throws<FonepayError>(() => new FonepayClient());
|
|
||||||
}
|
|
||||||
|
|
||||||
[UnityTest]
|
|
||||||
public IEnumerator AwaitPaymentAsync_EmptyUrl_Throws() => RunAsync(async () =>
|
|
||||||
{
|
|
||||||
var client = TryBuildClient();
|
|
||||||
if (client == null) Assert.Pass("Skipped: no FonepayConfig in test context.");
|
|
||||||
|
|
||||||
Assert.ThrowsAsync<ArgumentException>(async () =>
|
|
||||||
await client.AwaitPaymentAsync(string.Empty));
|
|
||||||
});
|
|
||||||
|
|
||||||
[UnityTest]
|
|
||||||
public IEnumerator AwaitPaymentAsync_PreCancelled_Throws() => RunAsync(async () =>
|
|
||||||
{
|
|
||||||
var client = TryBuildClient();
|
|
||||||
if (client == null) Assert.Pass("Skipped: no FonepayConfig in test context.");
|
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource();
|
|
||||||
cts.Cancel();
|
|
||||||
|
|
||||||
Assert.ThrowsAsync<OperationCanceledException>(async () =>
|
|
||||||
await client.AwaitPaymentAsync("ws://127.0.0.1:1/", ct: cts.Token));
|
|
||||||
});
|
|
||||||
|
|
||||||
private static FonepayClient TryBuildClient()
|
|
||||||
{
|
|
||||||
try { return new FonepayClient(); }
|
|
||||||
catch (FonepayError) { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerator RunAsync(Func<Task> body)
|
|
||||||
{
|
|
||||||
var t = body();
|
|
||||||
while (!t.IsCompleted) yield return null;
|
|
||||||
if (t.IsFaulted) throw t.Exception;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ EditorBuildSettings:
|
|||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Scenes:
|
m_Scenes:
|
||||||
- enabled: 1
|
- enabled: 1
|
||||||
path: Assets/Scenes/SampleScene.unity
|
path: Packages/com.darkmattergameproduction.fonepay-unity/Samples/Example/SampleScene.unity
|
||||||
guid: 8c9cfa26abfee488c85f1582747f6a02
|
guid: 8c9cfa26abfee488c85f1582747f6a02
|
||||||
m_configObjects:
|
m_configObjects:
|
||||||
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}
|
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}
|
||||||
|
|||||||
239
README.md
Normal file
239
README.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# Fonepay Unity
|
||||||
|
|
||||||
|
Fonepay payment SDK for Unity. Generate Fonepay QR codes, await payment confirmation over websocket, and process tax refunds — all from a single async-friendly client.
|
||||||
|
|
||||||
|
This repo is a **Unity 6000.4.5f1** project that hosts the package source under `Packages/com.darkmattergameproduction.fonepay-unity/` and a sample scene under `Assets/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- `FonepayClient` façade — `PurchaseAsync`, `GetStatusAsync`, `AwaitPaymentAsync`, `PostTaxRefundAsync`.
|
||||||
|
- HMAC-SHA512 dataValidation on all signed payloads.
|
||||||
|
- Websocket payment listener with cancellation, timeout, and clean disconnect.
|
||||||
|
- QR rendered straight into a `Texture2D` ready to assign to `UnityEngine.UI.Image`.
|
||||||
|
- Editor-side credential management (Tools > Fonepay > Settings) — secrets baked at build time, never committed.
|
||||||
|
- NUnit edit-mode + play-mode tests.
|
||||||
|
- Importable sample under Package Manager > Samples.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Unity | 6000.4.5f1+ |
|
||||||
|
| Test Framework | `com.unity.test-framework` 1.6.0+ |
|
||||||
|
| UniTask | for the sample only — runtime uses `System.Threading.Tasks` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
### Via Unity Package Manager (recommended)
|
||||||
|
|
||||||
|
**Window > Package Manager > + > Install package from git URL**
|
||||||
|
|
||||||
|
```
|
||||||
|
https://github.com/Savya-lol/Fonepay-Unity.git#0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Or edit `Packages/manifest.json` directly:
|
||||||
|
```json
|
||||||
|
"com.darkmattergameproduction.fonepay-unity": "https://github.com/Savya-lol/Fonepay-Unity.git#0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pin to any released tag (e.g. `#0.1.0`) or track the floating `upm` branch (`#upm`). The `upm` branch is the package subtree, auto-published from `main` via GitHub Actions.
|
||||||
|
|
||||||
|
### As a local clone of this repo
|
||||||
|
|
||||||
|
1. Clone:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Savya-lol/Fonepay-Unity.git "Fonepay Unity"
|
||||||
|
```
|
||||||
|
2. Open in Unity Hub (6000.4.5f1).
|
||||||
|
|
||||||
|
### Setup credentials
|
||||||
|
|
||||||
|
1. **Tools > Fonepay > Settings** — create the `FonepayConfig` asset under `Assets/Resources/FonepayConfig.asset`.
|
||||||
|
2. Enter merchant code, username, password, and HMAC secret. The window stores secrets outside source control; a build preprocessor injects them as `FonepayBakedSecrets` and removes the resource after build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Request a QR
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Darkmatter.Fonepay;
|
||||||
|
|
||||||
|
var fonepay = new FonepayClient();
|
||||||
|
|
||||||
|
QrResult qr = await fonepay.PurchaseAsync(new QrRequest
|
||||||
|
{
|
||||||
|
amount = 100f,
|
||||||
|
remarks1 = "order #1234",
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
qrImage.sprite = Sprite.Create(
|
||||||
|
qr.qrCode,
|
||||||
|
new Rect(0, 0, qr.qrCode.width, qr.qrCode.height),
|
||||||
|
new Vector2(0.5f, 0.5f));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Await payment
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
QRPaymentStatus result = await fonepay.AwaitPaymentAsync(
|
||||||
|
qr.thirdpartyQrWebSocketUrl,
|
||||||
|
onQrVerified: scanned => Debug.Log($"QR scanned: {scanned}"),
|
||||||
|
ct: ct);
|
||||||
|
|
||||||
|
switch (result.Outcome)
|
||||||
|
{
|
||||||
|
case PaymentOutcome.Complete: break; // success
|
||||||
|
case PaymentOutcome.CancelledByUser: break; // dismissed in app
|
||||||
|
case PaymentOutcome.Failed: break; // server rejected
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cancel / timeout
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var cts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
cts.CancelAfter(TimeSpan.FromMinutes(15)); // QR validity window
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var payment = await fonepay.AwaitPaymentAsync(qr.thirdpartyQrWebSocketUrl, ct: cts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { /* user cancel or timeout */ }
|
||||||
|
catch (InvalidOperationException) { /* server closed websocket early */ }
|
||||||
|
catch (FonepayError e) { /* API error: e.ErrorCode, e.Docs */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
`cts.Cancel()` from a button handler aborts the await and disconnects the socket.
|
||||||
|
|
||||||
|
### Status poll
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
QrResult status = await fonepay.GetStatusAsync(prn, ct);
|
||||||
|
// status.message: "PAID" | "UNPAID" | error
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tax refund
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
TaxRefundResponse refund = await fonepay.PostTaxRefundAsync(new TaxRefundRequest
|
||||||
|
{
|
||||||
|
fonepayTraceId = "...",
|
||||||
|
merchantPRN = "...",
|
||||||
|
invoiceNumber = "INV-001",
|
||||||
|
invoiceDate = DateTime.UtcNow,
|
||||||
|
transactionAmount = 100f,
|
||||||
|
}, ct);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API surface
|
||||||
|
|
||||||
|
| Type | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `FonepayClient` | Public façade, auto-loads config + secrets. |
|
||||||
|
| `QrRequest` / `QrResult` | QR request/response DTOs. |
|
||||||
|
| `QRPaymentStatus` | Terminal payment frame, exposes `Outcome`. |
|
||||||
|
| `QRVerificationStatus` | Intermediate "QR scanned" frame. |
|
||||||
|
| `PaymentOutcome` | `Complete` / `CancelledByUser` / `Failed`. |
|
||||||
|
| `TaxRefundRequest` / `TaxRefundResponse` | Refund DTOs. |
|
||||||
|
| `FonepayError` | API-level exception with `ErrorCode` + `Docs`. |
|
||||||
|
|
||||||
|
### `Outcome` rules
|
||||||
|
|
||||||
|
| `success` | `paymentSuccess` | `Outcome` |
|
||||||
|
|---|---|---|
|
||||||
|
| true | true | Complete |
|
||||||
|
| true | false | CancelledByUser |
|
||||||
|
| false | * | Failed |
|
||||||
|
|
||||||
|
### Termination of `AwaitPaymentAsync`
|
||||||
|
|
||||||
|
| Condition | Result |
|
||||||
|
|---|---|
|
||||||
|
| Payment frame received | resolves with `QRPaymentStatus` |
|
||||||
|
| `CancellationToken` cancelled | throws `OperationCanceledException` |
|
||||||
|
| Server closes socket before payment | throws `InvalidOperationException` |
|
||||||
|
| Network/HTTP error during connect | throws `WebSocketException` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repo layout
|
||||||
|
|
||||||
|
```
|
||||||
|
Assets/ Sample scene + test MonoBehaviour
|
||||||
|
Packages/
|
||||||
|
com.darkmattergameproduction.fonepay-unity/
|
||||||
|
Runtime/
|
||||||
|
Core/ FonepayClient, FonepayConfig, FonepayConfigSO
|
||||||
|
API/ FonepayApiClient (REST), FonepayWebsocketClient
|
||||||
|
Crypto/ HmacSha512Signer
|
||||||
|
Models/ DTOs (QrRequest, QrResult, …)
|
||||||
|
QR/ FonepayQRGenerator
|
||||||
|
Editor/ Settings window, build secrets injector
|
||||||
|
Samples~/ Importable example
|
||||||
|
Tests/
|
||||||
|
Editor/ Edit-mode unit tests (NUnit)
|
||||||
|
Runtime/ Play-mode tests
|
||||||
|
Documentation/ Architecture notes
|
||||||
|
README.md Package-level docs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
**Window > General > Test Runner**
|
||||||
|
|
||||||
|
- **EditMode** — `WebsocketMessage<T>.Status` parsing, `PaymentOutcome` truth table, `FonepayError`.
|
||||||
|
- **PlayMode** — runtime parse sanity, missing-config throw, empty-URL throw, pre-cancelled token throw.
|
||||||
|
|
||||||
|
CLI (Unity 6 batchmode):
|
||||||
|
```bash
|
||||||
|
"<Unity>" -batchmode -nographics -projectPath . -runTests \
|
||||||
|
-testPlatform EditMode -testResults Logs/edit-tests.xml -quit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Credentials never live in this repo. `FonepayConfigSO` holds only public fields; password + HMAC secret are stored via `FonepaySecretsStore` (EditorPrefs) and baked into a temp `FonepayBakedSecrets` resource at build, then removed.
|
||||||
|
- All signed requests use HMAC-SHA512 — see `HmacSha512Signer`.
|
||||||
|
- `FonepayConfig.Invalidate()` clears the cached config (use after rotating credentials in Editor).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| `FonepayError: FonepayConfig asset missing` | No `Resources/FonepayConfig.asset` | Tools > Fonepay > Settings → Save |
|
||||||
|
| `FonepayError: Fonepay credentials missing` | Secrets not entered (Editor) or baked secrets stripped (build) | Re-open Settings window |
|
||||||
|
| `AwaitPaymentAsync` returns empty/default `QRPaymentStatus` | Old build pre-fix on `WebsocketMessage<T>.Status` | Update to current; status now reparses each access |
|
||||||
|
| `AwaitPaymentAsync` hangs forever | Server closed socket without payment frame | Update to current; now throws `InvalidOperationException` |
|
||||||
|
| `transactionDate` always default | `DateTime` not supported by `JsonUtility` | Fixed — field is `string` now |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Branch from `main`.
|
||||||
|
2. Run **Test Runner** (EditMode + PlayMode) before opening a PR.
|
||||||
|
3. Update `CHANGELOG.md` in the package.
|
||||||
|
4. Keep `FonepayConfig.asset` and any baked secrets out of commits.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See `Packages/com.darkmattergameproduction.fonepay-unity/Third Party Notices.md` for third-party attributions (QRCoder, UniTask). Project license: TBD.
|
||||||
Reference in New Issue
Block a user