From 9fb16ecf1a611a8ef0bee2f0fc18922fdfa656cc Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Mon, 30 Mar 2026 22:03:35 -0400 Subject: [PATCH 1/3] Flatten CreateTemplateRequest params to match API Replace nested Design/SupportInfo objects with flat fields (background_color, label_color, support_url, etc.) matching the API's template_params. Add Metadata field. Update README example and bump version to 1.3.0. --- AccessGridTest/AccessGridTest.csproj | 2 +- AccessGridTest/ConsoleServiceTests.cs | 66 ++++++++++++++++++++++++++- README.md | 30 ++++++------ src/AccessGrid.csproj | 2 +- src/AccessGrid/AccessGridClient.cs | 2 +- src/AccessGrid/Models.cs | 54 +++++++++++++++++++--- 6 files changed, 128 insertions(+), 28 deletions(-) diff --git a/AccessGridTest/AccessGridTest.csproj b/AccessGridTest/AccessGridTest.csproj index b61ae27..e959efc 100644 --- a/AccessGridTest/AccessGridTest.csproj +++ b/AccessGridTest/AccessGridTest.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 enable enable false diff --git a/AccessGridTest/ConsoleServiceTests.cs b/AccessGridTest/ConsoleServiceTests.cs index be667cc..59ffa50 100644 --- a/AccessGridTest/ConsoleServiceTests.cs +++ b/AccessGridTest/ConsoleServiceTests.cs @@ -165,7 +165,8 @@ public async Task CreateTemplateAsync_ShouldPostAndReturnTemplate() "protocol": "desfire", "created_at": "2025-03-01T00:00:00Z", "issued_keys_count": 0, - "active_keys_count": 0 + "active_keys_count": 0, + "metadata": { "version": "2.1" } } """; StubHttpResponse(json); @@ -178,7 +179,20 @@ public async Task CreateTemplateAsync_ShouldPostAndReturnTemplate() Protocol = Protocol.DESFire, AllowOnMultipleDevices = true, WatchCount = 2, - IPhoneCount = 3 + IPhoneCount = 3, + BackgroundColor = "#FFFFFF", + LabelColor = "#000000", + LabelSecondaryColor = "#333333", + SupportUrl = "https://help.yourcompany.com", + SupportPhoneNumber = "+1-555-123-4567", + SupportEmail = "support@yourcompany.com", + PrivacyPolicyUrl = "https://yourcompany.com/privacy", + TermsAndConditionsUrl = "https://yourcompany.com/terms", + Metadata = new Dictionary + { + ["version"] = "2.1", + ["approval_status"] = "approved" + } }; var result = await _client.Console.CreateTemplateAsync(request); @@ -196,6 +210,54 @@ public async Task CreateTemplateAsync_ShouldPostAndReturnTemplate() )), Times.Once); } + [Test] + public async Task CreateTemplateAsync_SendsFlatDesignAndSupportParams() + { + var json = """ + { + "id": "tmpl-flat", + "name": "Flat Params Template", + "platform": "apple", + "use_case": "employee_badge", + "protocol": "desfire", + "metadata": { "version": "2.1" } + } + """; + + string capturedBody = null; + _mockHttpClient + .Setup(x => x.SendAsync(It.IsAny())) + .Returns(async req => + { + if (req.Content != null) + capturedBody = await req.Content.ReadAsStringAsync(); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + }); + + var request = new CreateTemplateRequest + { + Name = "Flat Params Template", + Platform = Platform.Apple, + UseCase = "employee_badge", + Protocol = Protocol.DESFire, + BackgroundColor = "#FFFFFF", + SupportUrl = "https://help.yourcompany.com", + Metadata = new Dictionary { ["version"] = "2.1" } + }; + + await _client.Console.CreateTemplateAsync(request); + + Assert.That(capturedBody, Is.Not.Null); + // Flat params should appear at root level, not nested under design/support_info + Assert.That(capturedBody, Does.Contain("background_color")); + Assert.That(capturedBody, Does.Contain("support_url")); + Assert.That(capturedBody, Does.Not.Contain("\"design\"")); + Assert.That(capturedBody, Does.Not.Contain("\"support_info\"")); + } + #endregion #region UpdateTemplateAsync diff --git a/README.md b/README.md index 6a7d1f8..9841a94 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Official C# SDK for interacting with the AccessGrid API. ## Installation ``` -Install-Package accessgrid -Version 1.2.2 +Install-Package accessgrid -Version 1.3.0 ``` ## Authentication @@ -232,29 +232,25 @@ public async Task CreateTemplateAsync() var template = await client.Console.CreateTemplateAsync(new CreateTemplateRequest { - Name = "Employee NFC key", + Name = "Employee Access Pass", Platform = "apple", UseCase = "employee_badge", Protocol = "desfire", AllowOnMultipleDevices = true, WatchCount = 2, IPhoneCount = 3, - Design = new TemplateDesign + BackgroundColor = "#FFFFFF", + LabelColor = "#000000", + LabelSecondaryColor = "#333333", + SupportUrl = "https://help.yourcompany.com", + SupportPhoneNumber = "+1-555-123-4567", + SupportEmail = "support@yourcompany.com", + PrivacyPolicyUrl = "https://yourcompany.com/privacy", + TermsAndConditionsUrl = "https://yourcompany.com/terms", + Metadata = new Dictionary { - BackgroundColor = "#FFFFFF", - LabelColor = "#000000", - LabelSecondaryColor = "#333333", - BackgroundImage = "[image_in_base64_encoded_format]", - LogoImage = "[image_in_base64_encoded_format]", - IconImage = "[image_in_base64_encoded_format]" - }, - SupportInfo = new SupportInfo - { - SupportUrl = "https://help.yourcompany.com", - SupportPhoneNumber = "+1-555-123-4567", - SupportEmail = "support@yourcompany.com", - PrivacyPolicyUrl = "https://yourcompany.com/privacy", - TermsAndConditionsUrl = "https://yourcompany.com/terms" + ["version"] = "2.1", + ["approval_status"] = "approved" } }); diff --git a/src/AccessGrid.csproj b/src/AccessGrid.csproj index 27b2d5f..0673fbd 100644 --- a/src/AccessGrid.csproj +++ b/src/AccessGrid.csproj @@ -4,7 +4,7 @@ netstandard2.0 8.0 accessgrid - 1.2.2 + 1.3.0 AccessGrid AccessGrid Official C# SDK for the AccessGrid API diff --git a/src/AccessGrid/AccessGridClient.cs b/src/AccessGrid/AccessGridClient.cs index 6d06a8d..9e38899 100644 --- a/src/AccessGrid/AccessGridClient.cs +++ b/src/AccessGrid/AccessGridClient.cs @@ -19,7 +19,7 @@ public class AccessGridClient : IAccessGridClient, IApiService private readonly string _accountId; private readonly string _secretKey; private readonly JsonSerializerOptions _jsonOptions; - private const string Version = "1.0.0"; + private const string Version = "1.3.0"; /// /// Service for managing access cards diff --git a/src/AccessGrid/Models.cs b/src/AccessGrid/Models.cs index 7565ca8..82a7356 100644 --- a/src/AccessGrid/Models.cs +++ b/src/AccessGrid/Models.cs @@ -396,16 +396,58 @@ public class CreateTemplateRequest public int? IPhoneCount { get; set; } /// - /// Object representing card template design + /// Must be a 6 character hexadecimal value for the background color, i.e. #FFFFFF /// - [JsonPropertyName("design")] - public TemplateDesign Design { get; set; } + [JsonPropertyName("background_color")] + public string BackgroundColor { get; set; } /// - /// Information for users that shows up on the back of the NFC key + /// Must be a 6 character hexadecimal value for the label color, i.e. #000000 /// - [JsonPropertyName("support_info")] - public SupportInfo SupportInfo { get; set; } + [JsonPropertyName("label_color")] + public string LabelColor { get; set; } + + /// + /// Must be a 6 character hexadecimal value for the secondary label color, i.e. #333333 + /// + [JsonPropertyName("label_secondary_color")] + public string LabelSecondaryColor { get; set; } + + /// + /// Shows on the back of the issued NFC key + /// + [JsonPropertyName("support_url")] + public string SupportUrl { get; set; } + + /// + /// Shows on the back of the issued NFC key + /// + [JsonPropertyName("support_phone_number")] + public string SupportPhoneNumber { get; set; } + + /// + /// Shows on the back of the issued NFC key + /// + [JsonPropertyName("support_email")] + public string SupportEmail { get; set; } + + /// + /// Shows on the back of the issued NFC key + /// + [JsonPropertyName("privacy_policy_url")] + public string PrivacyPolicyUrl { get; set; } + + /// + /// Shows on the back of the issued NFC key + /// + [JsonPropertyName("terms_and_conditions_url")] + public string TermsAndConditionsUrl { get; set; } + + /// + /// Optional metadata key-value pairs + /// + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } } public class UpdateTemplateRequest From 85362579b6b501b912f13c3aaca11b3e6c994902 Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Mon, 30 Mar 2026 22:38:05 -0400 Subject: [PATCH 2/3] Flatten UpdateTemplateRequest params to match API Replace nested SupportInfo object with flat fields (background_color, label_color, support_url, etc.) matching the API's template_params. Update README example. --- AccessGridTest/ConsoleServiceTests.cs | 45 ++++++++++++++++++++++++- README.md | 18 +++++----- src/AccessGrid/Models.cs | 48 +++++++++++++++++++++++++-- 3 files changed, 98 insertions(+), 13 deletions(-) diff --git a/AccessGridTest/ConsoleServiceTests.cs b/AccessGridTest/ConsoleServiceTests.cs index 59ffa50..2ced0ec 100644 --- a/AccessGridTest/ConsoleServiceTests.cs +++ b/AccessGridTest/ConsoleServiceTests.cs @@ -282,7 +282,15 @@ public async Task UpdateTemplateAsync_ShouldPutAndReturnTemplate() Name = "Updated Badge", AllowOnMultipleDevices = false, WatchCount = 1, - IPhoneCount = 2 + IPhoneCount = 2, + BackgroundColor = "#FFFFFF", + LabelColor = "#000000", + LabelSecondaryColor = "#333333", + SupportUrl = "https://help.yourcompany.com", + SupportPhoneNumber = "+1-555-123-4567", + SupportEmail = "support@yourcompany.com", + PrivacyPolicyUrl = "https://yourcompany.com/privacy", + TermsAndConditionsUrl = "https://yourcompany.com/terms" }; var result = await _client.Console.UpdateTemplateAsync(request); @@ -296,6 +304,41 @@ public async Task UpdateTemplateAsync_ShouldPutAndReturnTemplate() )), Times.Once); } + [Test] + public async Task UpdateTemplateAsync_SendsFlatDesignAndSupportParams() + { + var json = """{ "id": "tmpl-123", "name": "Test" }"""; + + string capturedBody = null; + _mockHttpClient + .Setup(x => x.SendAsync(It.IsAny())) + .Returns(async req => + { + if (req.Content != null) + capturedBody = await req.Content.ReadAsStringAsync(); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + }); + + var request = new UpdateTemplateRequest + { + CardTemplateId = "tmpl-123", + Name = "Test", + BackgroundColor = "#FFFFFF", + SupportUrl = "https://help.yourcompany.com" + }; + + await _client.Console.UpdateTemplateAsync(request); + + Assert.That(capturedBody, Is.Not.Null); + Assert.That(capturedBody, Does.Contain("background_color")); + Assert.That(capturedBody, Does.Contain("support_url")); + Assert.That(capturedBody, Does.Not.Contain("\"design\"")); + Assert.That(capturedBody, Does.Not.Contain("\"support_info\"")); + } + #endregion #region ReadTemplateAsync diff --git a/README.md b/README.md index 9841a94..98008d5 100644 --- a/README.md +++ b/README.md @@ -276,18 +276,18 @@ public async Task UpdateTemplateAsync() new UpdateTemplateRequest { CardTemplateId = "0xd3adb00b5", - Name = "Updated Employee NFC key", + Name = "Updated Employee Access Pass", AllowOnMultipleDevices = true, WatchCount = 2, IPhoneCount = 3, - SupportInfo = new SupportInfo - { - SupportUrl = "https://help.yourcompany.com", - SupportPhoneNumber = "+1-555-123-4567", - SupportEmail = "support@yourcompany.com", - PrivacyPolicyUrl = "https://yourcompany.com/privacy", - TermsAndConditionsUrl = "https://yourcompany.com/terms" - } + BackgroundColor = "#FFFFFF", + LabelColor = "#000000", + LabelSecondaryColor = "#333333", + SupportUrl = "https://help.yourcompany.com", + SupportPhoneNumber = "+1-555-123-4567", + SupportEmail = "support@yourcompany.com", + PrivacyPolicyUrl = "https://yourcompany.com/privacy", + TermsAndConditionsUrl = "https://yourcompany.com/terms" } ); diff --git a/src/AccessGrid/Models.cs b/src/AccessGrid/Models.cs index 82a7356..7c0013b 100644 --- a/src/AccessGrid/Models.cs +++ b/src/AccessGrid/Models.cs @@ -483,10 +483,52 @@ public class UpdateTemplateRequest public int? IPhoneCount { get; set; } /// - /// Information for users that shows up on the back of the NFC key + /// Must be a 6 character hexadecimal value for the background color, i.e. #FFFFFF + /// + [JsonPropertyName("background_color")] + public string BackgroundColor { get; set; } + + /// + /// Must be a 6 character hexadecimal value for the label color, i.e. #000000 /// - [JsonPropertyName("support_info")] - public SupportInfo SupportInfo { get; set; } + [JsonPropertyName("label_color")] + public string LabelColor { get; set; } + + /// + /// Must be a 6 character hexadecimal value for the secondary label color, i.e. #333333 + /// + [JsonPropertyName("label_secondary_color")] + public string LabelSecondaryColor { get; set; } + + /// + /// Shows on the back of the issued NFC key + /// + [JsonPropertyName("support_url")] + public string SupportUrl { get; set; } + + /// + /// Shows on the back of the issued NFC key + /// + [JsonPropertyName("support_phone_number")] + public string SupportPhoneNumber { get; set; } + + /// + /// Shows on the back of the issued NFC key + /// + [JsonPropertyName("support_email")] + public string SupportEmail { get; set; } + + /// + /// Shows on the back of the issued NFC key + /// + [JsonPropertyName("privacy_policy_url")] + public string PrivacyPolicyUrl { get; set; } + + /// + /// Shows on the back of the issued NFC key + /// + [JsonPropertyName("terms_and_conditions_url")] + public string TermsAndConditionsUrl { get; set; } } public class EventLogFilters From 82127a5e995683d0a6a68896e7560b5f050c9fcc Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Mon, 30 Mar 2026 22:41:02 -0400 Subject: [PATCH 3/3] Add iOS preflight, webhooks CRUD, and HID orgs support Add IosPreflightAsync to ConsoleService for iOS In-App Provisioning. Add WebhooksService with List/Create/Delete methods. Add HIDService with nested Orgs service (Create/List/Activate). Add DeleteAsync to IApiService and AccessGridClient. Add models: IosPreflightResponse, Webhook, WebhooksResponse, CreateWebhookRequest, HIDOrg, CreateHIDOrgRequest, CompleteHIDOrgRequest. Add feature matrix to README. --- AccessGridTest/ConsoleServiceTests.cs | 237 ++++++++++++++++++++++++++ README.md | 167 ++++++++++++++++++ src/AccessGrid/AccessGridClient.cs | 8 + src/AccessGrid/ConsoleService.cs | 131 ++++++++++++++ src/AccessGrid/IApiService.cs | 7 + src/AccessGrid/Models.cs | 141 +++++++++++++++ 6 files changed, 691 insertions(+) diff --git a/AccessGridTest/ConsoleServiceTests.cs b/AccessGridTest/ConsoleServiceTests.cs index 2ced0ec..6349044 100644 --- a/AccessGridTest/ConsoleServiceTests.cs +++ b/AccessGridTest/ConsoleServiceTests.cs @@ -716,4 +716,241 @@ public async Task GetLedgerItemsAsync_TemporaryIsNull_WhenAbsentFromAccessPass() } #endregion + + #region IosPreflightAsync + + [Test] + public async Task IosPreflightAsync_ShouldReturnPreflightIdentifiers() + { + var json = """ + { + "provisioningCredentialIdentifier": "prov-cred-123", + "sharingInstanceIdentifier": "sharing-456", + "cardTemplateIdentifier": "tmpl-789", + "environmentIdentifier": "env-abc" + } + """; + StubHttpResponse(json); + + var result = await _client.Console.IosPreflightAsync("tmpl-789", "pass-456"); + + Assert.That(result.ProvisioningCredentialIdentifier, Is.EqualTo("prov-cred-123")); + Assert.That(result.SharingInstanceIdentifier, Is.EqualTo("sharing-456")); + Assert.That(result.CardTemplateIdentifier, Is.EqualTo("tmpl-789")); + Assert.That(result.EnvironmentIdentifier, Is.EqualTo("env-abc")); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Post && + req.RequestUri!.ToString().Contains("/v1/console/card-templates/tmpl-789/ios_preflight") + )), Times.Once); + } + + #endregion + + #region WebhooksService + + [Test] + public async Task WebhooksListAsync_ShouldReturnWebhooks() + { + var json = """ + { + "webhooks": [ + { + "id": "wh_1", + "name": "My Webhook", + "url": "https://example.com/webhook", + "auth_method": "bearer_token", + "subscribed_events": ["ag.access_pass.issued"], + "created_at": "2025-03-01T00:00:00Z" + } + ], + "pagination": { "current_page": 1, "per_page": 50, "total_pages": 1, "total_count": 1 } + } + """; + StubHttpResponse(json); + + var result = await _client.Console.Webhooks.ListAsync(); + + Assert.That(result.Webhooks, Has.Count.EqualTo(1)); + Assert.That(result.Webhooks[0].Id, Is.EqualTo("wh_1")); + Assert.That(result.Webhooks[0].Name, Is.EqualTo("My Webhook")); + Assert.That(result.Webhooks[0].Url, Is.EqualTo("https://example.com/webhook")); + Assert.That(result.Webhooks[0].AuthMethod, Is.EqualTo("bearer_token")); + Assert.That(result.Webhooks[0].SubscribedEvents, Has.Count.EqualTo(1)); + Assert.That(result.Pagination.TotalCount, Is.EqualTo(1)); + } + + [Test] + public async Task WebhooksCreateAsync_ShouldCreateWebhook() + { + var json = """ + { + "id": "wh_new", + "name": "New Webhook", + "url": "https://example.com/hook", + "auth_method": "bearer_token", + "subscribed_events": ["ag.access_pass.issued"], + "created_at": "2025-03-01T00:00:00Z", + "private_key": "secret-key-123" + } + """; + StubHttpResponse(json); + + var result = await _client.Console.Webhooks.CreateAsync(new CreateWebhookRequest + { + Name = "New Webhook", + Url = "https://example.com/hook", + SubscribedEvents = new List { "ag.access_pass.issued" } + }); + + Assert.That(result.Id, Is.EqualTo("wh_new")); + Assert.That(result.PrivateKey, Is.EqualTo("secret-key-123")); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Post && + req.RequestUri!.ToString().Contains("/v1/console/webhooks") + )), Times.Once); + } + + [Test] + public async Task WebhooksDeleteAsync_ShouldDeleteWebhook() + { + StubHttpResponse("{}"); + + await _client.Console.Webhooks.DeleteAsync("wh_123"); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Delete && + req.RequestUri!.ToString().Contains("/v1/console/webhooks/wh_123") + )), Times.Once); + } + + #endregion + + #region HIDOrgsService + + [Test] + public async Task HIDOrgsCreateAsync_ShouldCreateOrg() + { + var json = """ + { + "id": "org_1", + "name": "My Org", + "slug": "my-org", + "first_name": "Ada", + "last_name": "Lovelace", + "phone": "+1-555-0000", + "full_address": "1 Main St, NY NY", + "status": "pending", + "created_at": "2025-03-01T00:00:00Z" + } + """; + StubHttpResponse(json); + + var result = await _client.Console.HID.Orgs.CreateAsync(new CreateHIDOrgRequest + { + Name = "My Org", + FullAddress = "1 Main St, NY NY", + Phone = "+1-555-0000", + FirstName = "Ada", + LastName = "Lovelace" + }); + + Assert.That(result.Id, Is.EqualTo("org_1")); + Assert.That(result.Name, Is.EqualTo("My Org")); + Assert.That(result.Slug, Is.EqualTo("my-org")); + Assert.That(result.FirstName, Is.EqualTo("Ada")); + Assert.That(result.Status, Is.EqualTo("pending")); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Post && + req.RequestUri!.ToString().Contains("/v1/console/hid/orgs") + )), Times.Once); + } + + [Test] + public async Task HIDOrgsListAsync_ShouldReturnOrgs() + { + var json = """ + [ + { + "id": "org_1", + "name": "Org One", + "slug": "org-one", + "first_name": "Ada", + "last_name": "Lovelace", + "phone": "+1-555-0000", + "full_address": "1 Main St", + "status": "active", + "created_at": "2025-03-01T00:00:00Z" + }, + { + "id": "org_2", + "name": "Org Two", + "slug": "org-two", + "first_name": "Bob", + "last_name": "Smith", + "phone": "+1-555-1111", + "full_address": "2 Main St", + "status": "pending", + "created_at": "2025-03-02T00:00:00Z" + } + ] + """; + StubHttpResponse(json); + + var result = await _client.Console.HID.Orgs.ListAsync(); + + Assert.That(result, Has.Count.EqualTo(2)); + Assert.That(result[0].Id, Is.EqualTo("org_1")); + Assert.That(result[0].Name, Is.EqualTo("Org One")); + Assert.That(result[1].Id, Is.EqualTo("org_2")); + Assert.That(result[1].Status, Is.EqualTo("pending")); + } + + [Test] + public async Task HIDOrgsListAsync_ShouldHandleEmptyList() + { + StubHttpResponse("[]"); + + var result = await _client.Console.HID.Orgs.ListAsync(); + + Assert.That(result, Is.Empty); + } + + [Test] + public async Task HIDOrgsActivateAsync_ShouldCompleteRegistration() + { + var json = """ + { + "id": "org_1", + "name": "My Org", + "slug": "my-org", + "first_name": "Ada", + "last_name": "Lovelace", + "phone": "+1-555-0000", + "full_address": "1 Main St", + "status": "active", + "created_at": "2025-03-01T00:00:00Z" + } + """; + StubHttpResponse(json); + + var result = await _client.Console.HID.Orgs.ActivateAsync(new CompleteHIDOrgRequest + { + Email = "admin@example.com", + Password = "hid-password-123" + }); + + Assert.That(result.Id, Is.EqualTo("org_1")); + Assert.That(result.Name, Is.EqualTo("My Org")); + Assert.That(result.Status, Is.EqualTo("active")); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Post && + req.RequestUri!.ToString().Contains("/v1/console/hid/orgs/activate") + )), Times.Once); + } + + #endregion } diff --git a/README.md b/README.md index 98008d5..3c27604 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,147 @@ public async Task GetLedgerItemsAsync() } ``` +### iOS In-App Provisioning Preflight + +```csharp +using AccessGrid; +using System; +using System.Threading.Tasks; + +public async Task GenerateProvisioningCredentialsAsync() +{ + var client = new AccessGridClient( + Environment.GetEnvironmentVariable("ACCOUNT_ID"), + Environment.GetEnvironmentVariable("SECRET_KEY") + ); + + var response = await client.Console.IosPreflightAsync( + cardTemplateId: "0xt3mp14t3-3x1d", + accessPassExId: "0xp455-3x1d" + ); + + Console.WriteLine($"Provisioning Credential ID: {response.ProvisioningCredentialIdentifier}"); + Console.WriteLine($"Sharing Instance ID: {response.SharingInstanceIdentifier}"); + Console.WriteLine($"Card Template ID: {response.CardTemplateIdentifier}"); + Console.WriteLine($"Environment ID: {response.EnvironmentIdentifier}"); +} +``` + +### Webhooks + +```csharp +using AccessGrid; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +public async Task ManageWebhooksAsync() +{ + var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); + var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); + + using var client = new AccessGridClient(accountId, secretKey); + + // List webhooks + var webhooks = await client.Console.Webhooks.ListAsync(); + foreach (var wh in webhooks.Webhooks) + { + Console.WriteLine($"Webhook: {wh.Name} ({wh.Id})"); + } + + // Create a webhook + var newWebhook = await client.Console.Webhooks.CreateAsync(new CreateWebhookRequest + { + Name = "My Webhook", + Url = "https://example.com/webhook", + SubscribedEvents = new List { "ag.access_pass.issued", "ag.access_pass.activated" } + }); + Console.WriteLine($"Created webhook: {newWebhook.Id}"); + Console.WriteLine($"Private key: {newWebhook.PrivateKey}"); + + // Delete a webhook + await client.Console.Webhooks.DeleteAsync(newWebhook.Id); +} +``` + +### HID Organizations + +#### Create an HID org + +```csharp +using AccessGrid; +using System; +using System.Threading.Tasks; + +public async Task CreateOrgAsync() +{ + var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); + var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); + + var client = new AccessGridClient(accountId, secretKey); + + var org = await client.Console.HID.Orgs.CreateAsync(new CreateHIDOrgRequest + { + Name = "My Org", + FullAddress = "1 Main St, NY NY", + Phone = "+1-555-0000", + FirstName = "Ada", + LastName = "Lovelace" + }); + + Console.WriteLine($"Created org: {org.Name} (ID: {org.Id})"); + Console.WriteLine($"Slug: {org.Slug}"); +} +``` + +#### List HID orgs + +```csharp +using AccessGrid; +using System; +using System.Threading.Tasks; + +public async Task ListOrgsAsync() +{ + var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); + var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); + + var client = new AccessGridClient(accountId, secretKey); + + var orgs = await client.Console.HID.Orgs.ListAsync(); + + foreach (var org in orgs) + { + Console.WriteLine($"Org ID: {org.Id}, Name: {org.Name}, Slug: {org.Slug}"); + } +} +``` + +#### Activate an HID org + +```csharp +using AccessGrid; +using System; +using System.Threading.Tasks; + +public async Task CompleteRegistrationAsync() +{ + var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); + var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); + + var client = new AccessGridClient(accountId, secretKey); + + var result = await client.Console.HID.Orgs.ActivateAsync(new CompleteHIDOrgRequest + { + Email = "admin@example.com", + Password = "hid-password-123" + }); + + Console.WriteLine($"Completed registration for org: {result.Name}"); + Console.WriteLine($"Status: {result.Status}"); +} +``` + ## Testing Your Application Code When building applications that use the AccessGrid SDK, you'll want to test your own business logic without making actual API calls. Here are examples of how to test your application code that calls the AccessGrid library. @@ -749,6 +890,32 @@ public class AccessCardsApiTests } } +## Feature Matrix + +| Endpoint | Method | Supported | +|---|---|:---:| +| POST /v1/key-cards | `AccessCards.ProvisionAsync()` | Y | +| GET /v1/key-cards/{id} | `AccessCards.GetAsync()` | Y | +| PATCH /v1/key-cards/{id} | `AccessCards.UpdateAsync()` | Y | +| GET /v1/key-cards | `AccessCards.ListAsync()` | Y | +| POST /v1/key-cards/{id}/suspend | `AccessCards.SuspendAsync()` | Y | +| POST /v1/key-cards/{id}/resume | `AccessCards.ResumeAsync()` | Y | +| POST /v1/key-cards/{id}/unlink | `AccessCards.UnlinkAsync()` | Y | +| POST /v1/key-cards/{id}/delete | `AccessCards.DeleteAsync()` | Y | +| POST /v1/console/card-templates | `Console.CreateTemplateAsync()` | Y | +| PUT /v1/console/card-templates/{id} | `Console.UpdateTemplateAsync()` | Y | +| GET /v1/console/card-templates/{id} | `Console.ReadTemplateAsync()` | Y | +| GET /v1/console/card-templates/{id}/logs | `Console.EventLogAsync()` | Y | +| GET /v1/console/pass-template-pairs | `Console.ListPassTemplatePairsAsync()` | Y | +| POST /v1/console/card-templates/{id}/ios_preflight | `Console.IosPreflightAsync()` | Y | +| GET /v1/console/ledger-items | `Console.GetLedgerItemsAsync()` | Y | +| GET /v1/console/webhooks | `Console.Webhooks.ListAsync()` | Y | +| POST /v1/console/webhooks | `Console.Webhooks.CreateAsync()` | Y | +| DELETE /v1/console/webhooks/{id} | `Console.Webhooks.DeleteAsync()` | Y | +| POST /v1/console/hid/orgs | `Console.HID.Orgs.CreateAsync()` | Y | +| POST /v1/console/hid/orgs/activate | `Console.HID.Orgs.ActivateAsync()` | Y | +| GET /v1/console/hid/orgs | `Console.HID.Orgs.ListAsync()` | Y | + ## License MIT diff --git a/src/AccessGrid/AccessGridClient.cs b/src/AccessGrid/AccessGridClient.cs index 9e38899..6aa4135 100644 --- a/src/AccessGrid/AccessGridClient.cs +++ b/src/AccessGrid/AccessGridClient.cs @@ -100,6 +100,14 @@ public async Task PatchAsync(string endpoint, object data) { return await MakeRequestAsync(new HttpMethod("PATCH"), endpoint, data); } + + /// + /// Makes a DELETE request to the API + /// + public async Task DeleteAsync(string endpoint) + { + await MakeRequestAsync(HttpMethod.Delete, endpoint); + } #endregion #region Helpers diff --git a/src/AccessGrid/ConsoleService.cs b/src/AccessGrid/ConsoleService.cs index 2358366..7b05e75 100644 --- a/src/AccessGrid/ConsoleService.cs +++ b/src/AccessGrid/ConsoleService.cs @@ -10,9 +10,21 @@ public class ConsoleService { private readonly IApiService _apiService; + /// + /// HID-related services + /// + public HIDService HID { get; } + + /// + /// Webhook management services + /// + public WebhooksService Webhooks { get; } + internal ConsoleService(IApiService apiService) { _apiService = apiService; + HID = new HIDService(apiService); + Webhooks = new WebhooksService(apiService); } /// @@ -124,5 +136,124 @@ public async Task GetLedgerItemsAsync(int? page = null, int var response = await _apiService.GetAsync("/v1/console/ledger-items", queryParams); return response ?? new LedgerItemsResponse(); } + + /// + /// Retrieves iOS In-App Provisioning identifiers for a card template and access pass + /// + /// The card template ID + /// The access pass external ID + /// iOS preflight identifiers + public async Task IosPreflightAsync(string cardTemplateId, string accessPassExId) + { + var body = new { access_pass_ex_id = accessPassExId }; + var response = await _apiService.PostAsync($"/v1/console/card-templates/{cardTemplateId}/ios_preflight", body); + return response; + } + } + + /// + /// Service for managing webhooks + /// + public class WebhooksService + { + private readonly IApiService _apiService; + + internal WebhooksService(IApiService apiService) + { + _apiService = apiService; + } + + /// + /// Lists all webhooks + /// + public async Task ListAsync(int? page = null, int? perPage = null) + { + var queryParams = new Dictionary(); + + if (page.HasValue) + queryParams.Add("page", page.Value.ToString()); + + if (perPage.HasValue) + queryParams.Add("per_page", perPage.Value.ToString()); + + var response = await _apiService.GetAsync("/v1/console/webhooks", queryParams); + return response ?? new WebhooksResponse(); + } + + /// + /// Creates a new webhook + /// + public async Task CreateAsync(CreateWebhookRequest request) + { + if (string.IsNullOrEmpty(request.AuthMethod)) + request.AuthMethod = "bearer_token"; + + var response = await _apiService.PostAsync("/v1/console/webhooks", request); + return response; + } + + /// + /// Deletes a webhook by ID + /// + public async Task DeleteAsync(string webhookId) + { + await _apiService.DeleteAsync($"/v1/console/webhooks/{webhookId}"); + } + } + + /// + /// Service providing access to HID-related services + /// + public class HIDService + { + /// + /// HID organization management + /// + public HIDOrgsService Orgs { get; } + + internal HIDService(IApiService apiService) + { + Orgs = new HIDOrgsService(apiService); + } + } + + /// + /// Service for managing HID organizations + /// + public class HIDOrgsService + { + private readonly IApiService _apiService; + + internal HIDOrgsService(IApiService apiService) + { + _apiService = apiService; + } + + /// + /// Creates a new HID organization + /// + public async Task CreateAsync(CreateHIDOrgRequest request) + { + var response = await _apiService.PostAsync("/v1/console/hid/orgs", request); + return response; + } + + /// + /// Lists all HID organizations + /// + public async Task> ListAsync() + { + var response = await _apiService.GetAsync>("/v1/console/hid/orgs"); + return response ?? new List(); + } + + /// + /// Completes HID org registration with credentials + /// + public async Task ActivateAsync(CompleteHIDOrgRequest request) + { + var response = await _apiService.PostAsync("/v1/console/hid/orgs/activate", request); + return response; + } } } \ No newline at end of file diff --git a/src/AccessGrid/IApiService.cs b/src/AccessGrid/IApiService.cs index 24cfec5..b9ddecf 100644 --- a/src/AccessGrid/IApiService.cs +++ b/src/AccessGrid/IApiService.cs @@ -43,5 +43,12 @@ public interface IApiService /// Data to send in request body /// Deserialized response Task PatchAsync(string endpoint, object data); + + /// + /// Makes a DELETE request to the API + /// + /// API endpoint + /// Task + Task DeleteAsync(string endpoint); } } \ No newline at end of file diff --git a/src/AccessGrid/Models.cs b/src/AccessGrid/Models.cs index 7c0013b..2395f14 100644 --- a/src/AccessGrid/Models.cs +++ b/src/AccessGrid/Models.cs @@ -740,6 +740,147 @@ public class LedgerItemsResponse public PaginationInfo Pagination { get; set; } } + /// + /// iOS In-App Provisioning preflight response + /// + public class IosPreflightResponse + { + [JsonPropertyName("provisioningCredentialIdentifier")] + public string ProvisioningCredentialIdentifier { get; set; } + + [JsonPropertyName("sharingInstanceIdentifier")] + public string SharingInstanceIdentifier { get; set; } + + [JsonPropertyName("cardTemplateIdentifier")] + public string CardTemplateIdentifier { get; set; } + + [JsonPropertyName("environmentIdentifier")] + public string EnvironmentIdentifier { get; set; } + } + + /// + /// A webhook configuration + /// + public class Webhook + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("auth_method")] + public string AuthMethod { get; set; } + + [JsonPropertyName("subscribed_events")] + public List SubscribedEvents { get; set; } + + [JsonPropertyName("created_at")] + public string CreatedAt { get; set; } + + [JsonPropertyName("private_key")] + public string PrivateKey { get; set; } + } + + /// + /// Response wrapper for listing webhooks + /// + public class WebhooksResponse + { + [JsonPropertyName("webhooks")] + public List Webhooks { get; set; } = new List(); + + [JsonPropertyName("pagination")] + public PaginationInfo Pagination { get; set; } + } + + /// + /// Parameters for creating a webhook + /// + public class CreateWebhookRequest + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("auth_method")] + public string AuthMethod { get; set; } + + [JsonPropertyName("subscribed_events")] + public List SubscribedEvents { get; set; } + } + + /// + /// An HID organization + /// + public class HIDOrg + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("slug")] + public string Slug { get; set; } + + [JsonPropertyName("first_name")] + public string FirstName { get; set; } + + [JsonPropertyName("last_name")] + public string LastName { get; set; } + + [JsonPropertyName("phone")] + public string Phone { get; set; } + + [JsonPropertyName("full_address")] + public string FullAddress { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("created_at")] + public string CreatedAt { get; set; } + } + + /// + /// Parameters for creating an HID organization + /// + public class CreateHIDOrgRequest + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("full_address")] + public string FullAddress { get; set; } + + [JsonPropertyName("phone")] + public string Phone { get; set; } + + [JsonPropertyName("first_name")] + public string FirstName { get; set; } + + [JsonPropertyName("last_name")] + public string LastName { get; set; } + } + + /// + /// Parameters for completing HID org registration + /// + public class CompleteHIDOrgRequest + { + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonPropertyName("password")] + public string Password { get; set; } + } + /// /// The CloudEvents data of an access pass webhook event ///