diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4651e..73f6f15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] + +### Added + +- Optional `Iframe` parameter on `CreateInvoiceParams` to enable embedded (iframe) + checkout. Like `PaymentMode`, it is sent in the request body but excluded from the + HMAC signature. + ## [0.1.0] ### Added diff --git a/README.md b/README.md index 5d0ba29..06f25b5 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ public sealed class CheckoutController : ControllerBase Currency = "USD", RedirectionUrl = "https://shop.example.com/return", WebhookUrl = "https://shop.example.com/webhooks/paylink", + // Iframe = true, // optional: enable embedded (iframe) checkout }); return Redirect(checkout.CheckoutUrl); diff --git a/src/Paylink/Internal/FieldOrders.cs b/src/Paylink/Internal/FieldOrders.cs index d619850..81d2b18 100644 --- a/src/Paylink/Internal/FieldOrders.cs +++ b/src/Paylink/Internal/FieldOrders.cs @@ -59,6 +59,7 @@ internal static class FieldOrders new FieldSpec("webhook_url"), new FieldSpec("order_details"), new FieldSpec("payment_mode", false), + new FieldSpec("iframe", false), }); public static readonly EndpointSpec PaymentVoid = new EndpointSpec( diff --git a/src/Paylink/Paylink.csproj b/src/Paylink/Paylink.csproj index 4103ed7..f034b21 100644 --- a/src/Paylink/Paylink.csproj +++ b/src/Paylink/Paylink.csproj @@ -9,7 +9,7 @@ $(NoWarn);CS1591 Paylink - 0.1.0 + 0.2.0 GetPayIn Official server-side .NET SDK for the PayLink / GetPayIn payment integration API (checkouts, payment operations, card tokens, recurring mandates, webhook verification). paylink;getpayin;payments;payment-gateway;checkout;webhooks;cybersource diff --git a/src/Paylink/Resources/Invoices.cs b/src/Paylink/Resources/Invoices.cs index 95086d7..19c8a64 100644 --- a/src/Paylink/Resources/Invoices.cs +++ b/src/Paylink/Resources/Invoices.cs @@ -42,6 +42,7 @@ public async Task CreateAsync( { "webhook_url", parameters.WebhookUrl }, { "order_details", parameters.OrderDetails }, { "payment_mode", parameters.PaymentMode }, + { "iframe", parameters.Iframe }, }; var data = await Api diff --git a/src/Paylink/Types/Params.cs b/src/Paylink/Types/Params.cs index d875cd7..d22690e 100644 --- a/src/Paylink/Types/Params.cs +++ b/src/Paylink/Types/Params.cs @@ -36,6 +36,8 @@ public sealed class CreateInvoiceParams public string? OrderDetails { get; set; } public string? PaymentMode { get; set; } + + public bool? Iframe { get; set; } } /// diff --git a/tests/Paylink.Tests/IframeUnsignedTests.cs b/tests/Paylink.Tests/IframeUnsignedTests.cs new file mode 100644 index 0000000..20cb8f8 --- /dev/null +++ b/tests/Paylink.Tests/IframeUnsignedTests.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using Paylink.Internal; +using Xunit; + +namespace Paylink.Tests +{ + /// + /// The optional iframe field on invoice create is sent in the body but excluded + /// from the HMAC signature — exactly like payment_mode. These tests build the + /// signed body directly (as the golden tests do) to prove both properties. + /// + public sealed class IframeUnsignedTests + { + private const string PublicToken = "pub_token"; + private const string HashToken = "hash_token"; + + private static Dictionary BaseInput() + { + return new Dictionary + { + { "first_name", "John" }, + { "last_name", "Doe" }, + { "email", "john@example.com" }, + { "order_title", "Gold Plan" }, + { "order_amount", "250.00" }, + { "currency", "USD" }, + { "redirection_url", "https://shop.example.com/return" }, + { "webhook_url", "https://shop.example.com/webhooks/paylink" }, + }; + } + + [Fact] + public void IframeTrueGoesIntoBodyAsOne() + { + var input = BaseInput(); + input["iframe"] = true; + + var body = SignedBody.Build(FieldOrders.InvoiceCreate, input, PublicToken, HashToken); + + Assert.Equal("1", body["iframe"]); + } + + [Fact] + public void IframeFalseGoesIntoBodyAsZero() + { + var input = BaseInput(); + input["iframe"] = false; + + var body = SignedBody.Build(FieldOrders.InvoiceCreate, input, PublicToken, HashToken); + + Assert.Equal("0", body["iframe"]); + } + + [Fact] + public void OmittedIframeIsAbsentFromBody() + { + var body = SignedBody.Build(FieldOrders.InvoiceCreate, BaseInput(), PublicToken, HashToken); + + Assert.False(body.ContainsKey("iframe")); + } + + [Fact] + public void IframeDoesNotChangeTheSignature() + { + var withoutIframe = SignedBody.Build(FieldOrders.InvoiceCreate, BaseInput(), PublicToken, HashToken); + + var withIframe = new Dictionary(BaseInput()) + { + { "iframe", true }, + }; + var signed = SignedBody.Build(FieldOrders.InvoiceCreate, withIframe, PublicToken, HashToken); + + // iframe is present in the body but must not alter the signature. + Assert.Equal("1", signed["iframe"]); + Assert.Equal(withoutIframe["signature"], signed["signature"]); + } + } +}