Compare commits
4 Commits
906ebbcac9
...
6a8a6e46f0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a8a6e46f0 | ||
|
|
9f620084b2 | ||
|
|
3c17829453 | ||
|
|
846a4fda9c |
@@ -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
|
|
||||||
@@ -445,7 +445,6 @@ 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
|
||||||
@@ -515,22 +514,6 @@ 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: 57da22305386447fcabd663378359ede, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::Darkmatter.Fonepay.Samples.SamplePayment
|
|
||||||
qrImage: {fileID: 308706331}
|
|
||||||
successObject: {fileID: 454747}
|
|
||||||
failedObject: {fileID: 1400840764}
|
|
||||||
payButton: {fileID: 1002816889}
|
|
||||||
--- !u!1 &519420028
|
--- !u!1 &519420028
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# 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.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +69,14 @@ namespace Darkmatter.Fonepay
|
|||||||
ws.OnQrVerified += onQrVerified;
|
ws.OnQrVerified += onQrVerified;
|
||||||
|
|
||||||
ws.OnPaymentReceived += msg => tcs.TrySetResult(msg.Status);
|
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));
|
using var ctReg = ct.Register(() => tcs.TrySetCanceled(ct));
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 7f9dbf7477f88405ca98d50075ced835
|
guid: debdfefd8b01242e8b95f0e29bb09c0d
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
@@ -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.
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 111b87cef6134401aa05813da47709aa
|
guid: 3902a28335b7043bf831aab193145f4c
|
||||||
folderAsset: yes
|
TextScriptImporter:
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
assetBundleName:
|
assetBundleName:
|
||||||
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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
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()
|
||||||
|
{
|
||||||
|
var fonepay = new FonepayClient();
|
||||||
|
_payCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var qr = await fonepay.PurchaseAsync(
|
||||||
|
new QrRequest { amount = amount, remarks1 = "sample" },
|
||||||
|
_payCts.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: _payCts.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
|
||||||
|
{
|
||||||
|
_payCts.Dispose();
|
||||||
|
_payCts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowResult(bool ok)
|
||||||
|
{
|
||||||
|
qrImage.gameObject.SetActive(false);
|
||||||
|
if (successObject != null) successObject.SetActive(ok);
|
||||||
|
if (failedObject != null) failedObject.SetActive(!ok);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "Darkmatter.FonepayUnity.Editor.Tests",
|
||||||
|
"rootNamespace": "",
|
||||||
|
"references": [
|
||||||
|
"Darkmatter.FonepayUnity",
|
||||||
|
"Darkmatter.FonepayUnity.Editor",
|
||||||
|
"UnityEditor.TestRunner",
|
||||||
|
"UnityEngine.TestRunner"
|
||||||
|
],
|
||||||
|
"includePlatforms": [
|
||||||
|
"Editor"
|
||||||
|
],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": true,
|
||||||
|
"precompiledReferences": [
|
||||||
|
"nunit.framework.dll"
|
||||||
|
],
|
||||||
|
"autoReferenced": false,
|
||||||
|
"defineConstraints": [
|
||||||
|
"UNITY_INCLUDE_TESTS"
|
||||||
|
],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using Darkmatter.Fonepay;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Darkmatter.FonepayUnity.Editor.Tests
|
||||||
|
{
|
||||||
|
public class WebsocketMessageTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Status_ParsesNestedJsonString()
|
||||||
|
{
|
||||||
|
const string raw =
|
||||||
|
"{\"merchantId\":\"M1\",\"deviceId\":\"D1\"," +
|
||||||
|
"\"transactionStatus\":\"{\\\"paymentSuccess\\\":true,\\\"success\\\":true,\\\"amount\\\":42.5}\"}";
|
||||||
|
|
||||||
|
var envelope = JsonUtility.FromJson<WebsocketMessage<QRPaymentStatus>>(raw);
|
||||||
|
var status = envelope.Status;
|
||||||
|
|
||||||
|
Assert.IsTrue(status.success);
|
||||||
|
Assert.IsTrue(status.paymentSuccess);
|
||||||
|
Assert.AreEqual(42.5f, status.amount);
|
||||||
|
Assert.AreEqual(PaymentOutcome.Complete, status.Outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Status_QrVerifiedFrame()
|
||||||
|
{
|
||||||
|
const string raw =
|
||||||
|
"{\"transactionStatus\":\"{\\\"success\\\":true,\\\"qrVerified\\\":true,\\\"message\\\":\\\"ok\\\"}\"}";
|
||||||
|
|
||||||
|
var envelope = JsonUtility.FromJson<WebsocketMessage<QRVerificationStatus>>(raw);
|
||||||
|
var v = envelope.Status;
|
||||||
|
|
||||||
|
Assert.IsTrue(v.qrVerified);
|
||||||
|
Assert.AreEqual("ok", v.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PaymentOutcomeTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void SuccessTrue_PaymentTrue_Complete()
|
||||||
|
{
|
||||||
|
var s = new QRPaymentStatus { success = true, paymentSuccess = true };
|
||||||
|
Assert.AreEqual(PaymentOutcome.Complete, s.Outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SuccessTrue_PaymentFalse_CancelledByUser()
|
||||||
|
{
|
||||||
|
var s = new QRPaymentStatus { success = true, paymentSuccess = false };
|
||||||
|
Assert.AreEqual(PaymentOutcome.CancelledByUser, s.Outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SuccessFalse_Failed()
|
||||||
|
{
|
||||||
|
var s = new QRPaymentStatus { success = false, paymentSuccess = true };
|
||||||
|
Assert.AreEqual(PaymentOutcome.Failed, s.Outcome);
|
||||||
|
|
||||||
|
s = new QRPaymentStatus { success = false, paymentSuccess = false };
|
||||||
|
Assert.AreEqual(PaymentOutcome.Failed, s.Outcome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FonepayErrorTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Carries_CodeAndDocs()
|
||||||
|
{
|
||||||
|
var err = new FonepayError(42, "boom", "docs/url");
|
||||||
|
Assert.AreEqual(42, err.ErrorCode);
|
||||||
|
Assert.AreEqual("docs/url", err.Docs);
|
||||||
|
Assert.AreEqual("boom", err.Message);
|
||||||
|
StringAssert.Contains("ErrorCode: 42", err.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b507c7c2bcf2f403fb4157318b8d7e97
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "Darkmatter.FonepayUnity.Runtime.Tests",
|
||||||
|
"rootNamespace": "",
|
||||||
|
"references": [
|
||||||
|
"Darkmatter.FonepayUnity",
|
||||||
|
"UnityEngine.TestRunner"
|
||||||
|
],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": true,
|
||||||
|
"precompiledReferences": [
|
||||||
|
"nunit.framework.dll"
|
||||||
|
],
|
||||||
|
"autoReferenced": false,
|
||||||
|
"defineConstraints": [
|
||||||
|
"UNITY_INCLUDE_TESTS"
|
||||||
|
],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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()
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<ArgumentException>(async () =>
|
||||||
|
await client.AwaitPaymentAsync(string.Empty));
|
||||||
|
await Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator AwaitPaymentAsync_PreCancelled_Throws() => RunAsync(async () =>
|
||||||
|
{
|
||||||
|
var client = TryBuildClient();
|
||||||
|
if (client == null)
|
||||||
|
{
|
||||||
|
Assert.Pass("Skipped: no FonepayConfig in test context.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<OperationCanceledException>(async () =>
|
||||||
|
await client.AwaitPaymentAsync("ws://127.0.0.1:1/", ct: cts.Token));
|
||||||
|
await Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3eeb964c506324b58bd6f95c088096fa
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "com.darkmattergameproduction.fonepay-unity",
|
||||||
|
"displayName": "Fonepay Unity",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"unity": "6000.4",
|
||||||
|
"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.",
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.test-framework": "1.6.0"
|
||||||
|
},
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"displayName": "Example Payment Flow",
|
||||||
|
"description": "Minimal MonoBehaviour: request QR, render, await payment with cancel support.",
|
||||||
|
"path": "Samples~/Example"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "Savya Bikram Shah",
|
||||||
|
"url": "https://savya.com.np",
|
||||||
|
"email": "mail@savya.com.np"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# 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.1.0] - 2026-05-07
|
|
||||||
|
|
||||||
### This is the first release of *\<Fonepay Unity\>*.
|
|
||||||
|
|
||||||
*Short description of this release*
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
>>>
|
|
||||||
**_Package Documentation Template_**
|
|
||||||
|
|
||||||
Use this template to create preliminary, high-level documentation meant to introduce users to the feature and the sample files included in this package. When writing your documentation, do the following:
|
|
||||||
|
|
||||||
1. Follow instructions in blockquotes.
|
|
||||||
|
|
||||||
2. Replace angle brackets with the appropriate text. For example, replace "<package name>" with the official name of the package.
|
|
||||||
|
|
||||||
3. Delete sections that do not apply to your package. For example, a package containing only sample files does not have a "Using <package_name>" section, so this section can be removed.
|
|
||||||
|
|
||||||
4. After documentation is completed, make sure you delete all instructions and examples in blockquotes including this preamble and its title:
|
|
||||||
|
|
||||||
```
|
|
||||||
>>>
|
|
||||||
Delete all of the text between pairs of blockquote markdown.
|
|
||||||
>>>
|
|
||||||
```
|
|
||||||
>>>
|
|
||||||
|
|
||||||
# About <package name>
|
|
||||||
|
|
||||||
>>>
|
|
||||||
Name the heading of the first topic after the **displayName** of the package as it appears in the package manifest.
|
|
||||||
|
|
||||||
This first topic includes a brief, high-level explanation of the package and, if applicable, provides links to Unity Manual topics.
|
|
||||||
|
|
||||||
There are two types of packages:
|
|
||||||
|
|
||||||
- Packages that include features that augment the Unity Editor or Runtime.
|
|
||||||
- Packages that include sample files.
|
|
||||||
|
|
||||||
Choose one of the following introductory paragraphs that best fits the package:
|
|
||||||
>>>
|
|
||||||
|
|
||||||
Use the <package name> package to <list of the main uses for the package>. For example, use <package name> to create/generate/extend/capture <mention major use case, or a good example of what the package can be used for>. The <package name> package also includes <other relevant features or uses>.
|
|
||||||
|
|
||||||
> *or*
|
|
||||||
|
|
||||||
The <package name> package includes examples of <name of asset type, model, prefabs, and/or other GameObjects in the package>. For more information, see <xref to topic in the Unity Manual>.
|
|
||||||
|
|
||||||
>>>
|
|
||||||
**_Examples:_**
|
|
||||||
|
|
||||||
Here are some examples for reference only. Do not include these in the final documentation file:
|
|
||||||
|
|
||||||
*Use the Unity Recorder package to capture and save in-game data. For example, use Unity Recorder to record an mp4 file during a game session. The Unity Recorder package also includes an interface for setting-up and triggering recording sessions.*
|
|
||||||
|
|
||||||
*The Timeline Examples package includes examples of Timeline assets, Timeline Instances, animation, GameObjects, and scripts that illustrate how to use Unity's Timeline. For more information, see [ Unity's Timeline](https://docs.unity3d.com/Manual/TimelineSection.html) in the [Unity Manual](https://docs.unity3d.com). For licensing and usage, see Package Licensing.*
|
|
||||||
>>>
|
|
||||||
|
|
||||||
# Installing <package name>
|
|
||||||
>>>
|
|
||||||
Begin this section with a cross-reference to the official Unity Manual topic on how to install packages. If the package requires special installation instructions, include these steps in this section.
|
|
||||||
>>>
|
|
||||||
|
|
||||||
To install this package, follow the instructions in the [Package Manager documentation](https://docs.unity3d.com/Packages/com.unity.package-manager-ui@latest/index.html).
|
|
||||||
|
|
||||||
>>>
|
|
||||||
For some packages, there may be additional steps to complete the setup. You can add those here.
|
|
||||||
>>>
|
|
||||||
|
|
||||||
In addition, you need to install the following resources:
|
|
||||||
|
|
||||||
- <name of resource>: To install, open *Window > <name of menu item>*. The resource appears <at this location>.
|
|
||||||
- <name of sample>: To install, open *Window > <name of menu item>*. The new sample folder appears <at this location>.
|
|
||||||
|
|
||||||
|
|
||||||
<a name="UsingPackageName"></a>
|
|
||||||
# Using <package name>
|
|
||||||
>>>
|
|
||||||
The contents of this section depends on the type of package.
|
|
||||||
|
|
||||||
For packages that augment the Unity Editor with additional features, this section should include workflow and/or reference documentation:
|
|
||||||
|
|
||||||
* At a minimum, this section should include reference documentation that describes the windows, editors, and properties that the package adds to Unity. This reference documentation should include screen grabs (see how to add screens below), a list of settings, an explanation of what each setting does, and the default values of each setting.
|
|
||||||
* Ideally, this section should also include a workflow: a list of steps that the user can easily follow that demonstrates how to use the feature. This list of steps should include screen grabs (see how to add screens below) to better describe how to use the feature.
|
|
||||||
|
|
||||||
For packages that include sample files, this section may include detailed information on how the user can use these sample files in their projects and scenes. However, workflow diagrams or illustrations could be included if deemed appropriate.
|
|
||||||
|
|
||||||
## How to add images
|
|
||||||
|
|
||||||
*(This section is for reference. Do not include in the final documentation file)*
|
|
||||||
|
|
||||||
If the [Using <package name>](#UsingPackageName) section includes screen grabs or diagrams, a link to the image must be added to this MD file, before or after the paragraph with the instruction or description that references the image. In addition, a caption should be added to the image link that includes the name of the screen or diagram. All images must be PNG files with underscores for spaces. No animated GIFs.
|
|
||||||
|
|
||||||
An example is included below:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Notice that the example screen shot is included in the images folder. All screen grabs and/or diagrams must be added and referenced from the images folder.
|
|
||||||
|
|
||||||
For more on the Unity documentation standards for creating and adding screen grabs, see this confluence page: https://confluence.hq.unity3d.com/pages/viewpage.action?pageId=13500715
|
|
||||||
>>>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Technical details
|
|
||||||
## Requirements
|
|
||||||
>>>
|
|
||||||
This subtopic includes a bullet list with the compatible versions of Unity. This subtopic may also include additional requirements or recommendations for 3rd party software or hardware. An example includes a dependency on other packages. If you need to include references to non-Unity products, make sure you refer to these products correctly and that all references include the proper trademarks (tm or r)
|
|
||||||
>>>
|
|
||||||
|
|
||||||
This version of <package name> is compatible with the following versions of the Unity Editor:
|
|
||||||
|
|
||||||
* 2018.1 and later (recommended)
|
|
||||||
|
|
||||||
To use this package, you must have the following 3rd party products:
|
|
||||||
|
|
||||||
* <product name and version with trademark or registered trademark.>
|
|
||||||
* <product name and version with trademark or registered trademark.>
|
|
||||||
* <product name and version with trademark or registered trademark.>
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
>>>
|
|
||||||
This section lists the known limitations with this version of the package. If there are no known limitations, or if the limitations are trivial, exclude this section. An example is provided.
|
|
||||||
>>>
|
|
||||||
|
|
||||||
<package name> version <package version> includes the following known limitations:
|
|
||||||
|
|
||||||
* <brief one-line description of first limitation.>
|
|
||||||
* <brief one-line description of second limitation.>
|
|
||||||
* <and so on>
|
|
||||||
|
|
||||||
>>>
|
|
||||||
*Example (For reference. Do not include in the final documentation file):*
|
|
||||||
|
|
||||||
The Unity Recorder version 1.0 has the following limitations:*
|
|
||||||
|
|
||||||
* The Unity Recorder does not support sound.
|
|
||||||
* The Recorder window and Recorder properties are not available in standalone players.
|
|
||||||
* MP4 encoding is only available on Windows.
|
|
||||||
>>>
|
|
||||||
|
|
||||||
## Package contents
|
|
||||||
>>>
|
|
||||||
This section includes the location of important files you want the user to know about. For example, if this is a sample package containing textures, models, and materials separated by sample group, you may want to provide the folder location of each group.
|
|
||||||
>>>
|
|
||||||
|
|
||||||
The following table indicates the <describe the breakdown you used here>:
|
|
||||||
|
|
||||||
|Location|Description|
|
|
||||||
|---|---|
|
|
||||||
|`<folder>`|Contains <describe what the folder contains>.|
|
|
||||||
|`<file>`|Contains <describe what the file represents or implements>.|
|
|
||||||
|
|
||||||
>>>
|
|
||||||
*Example (For reference. Do not include in the final documentation file):*
|
|
||||||
|
|
||||||
The following table indicates the root folder of each type of sample in this package. Each sample's root folder contains its own Materials, Models, or Textures folders:
|
|
||||||
|
|
||||||
|Folder Location|Description|
|
|
||||||
|---|---|
|
|
||||||
|`WoodenCrate_Orange`|Root folder containing the assets for the orange crates.|
|
|
||||||
|`WoodenCrate_Mahogany`|Root folder containing the assets for the mahogany crates.|
|
|
||||||
|`WoodenCrate_Shared`|Root folder containing any material assets shared by all crates.|
|
|
||||||
>>>
|
|
||||||
|
|
||||||
## Document revision history
|
|
||||||
>>>
|
|
||||||
This section includes the revision history of the document. The revision history tracks when a document is created, edited, and updated. If you create or update a document, you must add a new row describing the revision. The Documentation Team also uses this table to track when a document is edited and its editing level. An example is provided:
|
|
||||||
|
|
||||||
|Date|Reason|
|
|
||||||
|---|---|
|
|
||||||
|Sept 12, 2017|Unedited. Published to package.|
|
|
||||||
|Sept 10, 2017|Document updated for package version 1.1.<br>New features: <li>audio support for capturing MP4s.<li>Instructions on saving Recorder prefabs|
|
|
||||||
|Sept 5, 2017|Limited edit by Documentation Team. Published to package.|
|
|
||||||
|Aug 25, 2017|Document created. Matches package version 1.0.|
|
|
||||||
>>>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
@@ -1,182 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4b1c61f6439b24b68bfbbe3420460669
|
|
||||||
TextureImporter:
|
|
||||||
internalIDToNameTable:
|
|
||||||
- first:
|
|
||||||
213: -8196121713257968172
|
|
||||||
second: example_0
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 13
|
|
||||||
mipmaps:
|
|
||||||
mipMapMode: 0
|
|
||||||
enableMipMap: 0
|
|
||||||
sRGBTexture: 1
|
|
||||||
linearTexture: 0
|
|
||||||
fadeOut: 0
|
|
||||||
borderMipMap: 0
|
|
||||||
mipMapsPreserveCoverage: 0
|
|
||||||
alphaTestReferenceValue: 0.5
|
|
||||||
mipMapFadeDistanceStart: 1
|
|
||||||
mipMapFadeDistanceEnd: 3
|
|
||||||
bumpmap:
|
|
||||||
convertToNormalMap: 0
|
|
||||||
externalNormalMap: 0
|
|
||||||
heightScale: 0.25
|
|
||||||
normalMapFilter: 0
|
|
||||||
flipGreenChannel: 0
|
|
||||||
isReadable: 0
|
|
||||||
streamingMipmaps: 0
|
|
||||||
streamingMipmapsPriority: 0
|
|
||||||
vTOnly: 0
|
|
||||||
ignoreMipmapLimit: 0
|
|
||||||
grayScaleToAlpha: 0
|
|
||||||
generateCubemap: 6
|
|
||||||
cubemapConvolution: 0
|
|
||||||
seamlessCubemap: 0
|
|
||||||
textureFormat: 1
|
|
||||||
maxTextureSize: 2048
|
|
||||||
textureSettings:
|
|
||||||
serializedVersion: 2
|
|
||||||
filterMode: 1
|
|
||||||
aniso: 1
|
|
||||||
mipBias: 0
|
|
||||||
wrapU: 1
|
|
||||||
wrapV: 1
|
|
||||||
wrapW: 1
|
|
||||||
nPOTScale: 0
|
|
||||||
lightmap: 0
|
|
||||||
compressionQuality: 50
|
|
||||||
spriteMode: 2
|
|
||||||
spriteExtrude: 1
|
|
||||||
spriteMeshType: 1
|
|
||||||
alignment: 0
|
|
||||||
spritePivot: {x: 0.5, y: 0.5}
|
|
||||||
spritePixelsToUnits: 100
|
|
||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
|
||||||
alphaUsage: 1
|
|
||||||
alphaIsTransparency: 1
|
|
||||||
spriteTessellationDetail: -1
|
|
||||||
textureType: 8
|
|
||||||
textureShape: 1
|
|
||||||
singleChannelComponent: 0
|
|
||||||
flipbookRows: 1
|
|
||||||
flipbookColumns: 1
|
|
||||||
maxTextureSizeSet: 0
|
|
||||||
compressionQualitySet: 0
|
|
||||||
textureFormatSet: 0
|
|
||||||
ignorePngGamma: 0
|
|
||||||
applyGammaDecoding: 0
|
|
||||||
swizzle: 50462976
|
|
||||||
cookieLightType: 0
|
|
||||||
platformSettings:
|
|
||||||
- serializedVersion: 4
|
|
||||||
buildTarget: DefaultTexturePlatform
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
ignorePlatformSupport: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
- serializedVersion: 4
|
|
||||||
buildTarget: Standalone
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
ignorePlatformSupport: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
- serializedVersion: 4
|
|
||||||
buildTarget: Android
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
ignorePlatformSupport: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
- serializedVersion: 4
|
|
||||||
buildTarget: WebGL
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
ignorePlatformSupport: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
- serializedVersion: 4
|
|
||||||
buildTarget: iOS
|
|
||||||
maxTextureSize: 2048
|
|
||||||
resizeAlgorithm: 0
|
|
||||||
textureFormat: -1
|
|
||||||
textureCompression: 1
|
|
||||||
compressionQuality: 50
|
|
||||||
crunchedCompression: 0
|
|
||||||
allowsAlphaSplitting: 0
|
|
||||||
overridden: 0
|
|
||||||
ignorePlatformSupport: 0
|
|
||||||
androidETC2FallbackOverride: 0
|
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
|
||||||
spriteSheet:
|
|
||||||
serializedVersion: 2
|
|
||||||
sprites:
|
|
||||||
- serializedVersion: 2
|
|
||||||
name: example_0
|
|
||||||
rect:
|
|
||||||
serializedVersion: 2
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: 748
|
|
||||||
height: 249
|
|
||||||
alignment: 0
|
|
||||||
pivot: {x: 0, y: 0}
|
|
||||||
border: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
customData:
|
|
||||||
outline: []
|
|
||||||
physicsShape: []
|
|
||||||
tessellationDetail: -1
|
|
||||||
bones: []
|
|
||||||
spriteID: 4dd84690ab6814e80800000000000000
|
|
||||||
internalID: -8196121713257968172
|
|
||||||
vertices: []
|
|
||||||
indices:
|
|
||||||
edges: []
|
|
||||||
weights: []
|
|
||||||
outline: []
|
|
||||||
customData:
|
|
||||||
physicsShape: []
|
|
||||||
bones: []
|
|
||||||
spriteID:
|
|
||||||
internalID: 0
|
|
||||||
vertices: []
|
|
||||||
indices:
|
|
||||||
edges: []
|
|
||||||
weights: []
|
|
||||||
secondaryTextures: []
|
|
||||||
spriteCustomMetadata:
|
|
||||||
entries: []
|
|
||||||
nameFileIdTable:
|
|
||||||
example_0: -8196121713257968172
|
|
||||||
mipmapLimitGroupName:
|
|
||||||
pSDRemoveMatte: 0
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Use this file to describe your package's features.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user