diff --git a/.vscode/settings.json b/.vscode/settings.json index 89bb090..d6d3033 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,6 @@ "CRS.FileNamePatternPageCustomizations": "..al", "CRS.RenameWithGit": false, "CRS.ObjectNamePrefix": "SPBPL ", - "al.ruleSetPath": "./ruleset.json", + "al.ruleSetPath": "src\\.rulesets\\SPBLicensing.ruleset.json", } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2971859..4c1e79a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ # Change Log + +## [2.0.202307.109] - 2023-02-16 +Massive refactoring. **Note: Breaking changes** + +### Added +- New Platform built in, [Lemon Squeezy](https://www.lemonsqueezy.com/) + +### Changed +- **Breaking Change:** All events refactored into a new Event Wrapper codeunit +- **Breaking Change:** Checking if a license is active moved to new codeunit for the purpose. This is in prep for more complex checking, such as metered usage scenarios +- **Breaking Change:** Interface for alternative platforms now has more procedure signatures to be more platform agnostic in logic flows. +- Restructure of Files/Folders to add developer clarity +- Refactor of entire Extension to comply better with SOLID principles +- Additional documentation added to codeunits/procedures + +### Notice + +Due to the sheer volume of breaking changes, the commit history on this repo is NOT completely detailing all changes. This repo is a PTE *clone* of the AppSource app (renamed and renumbered, so they can co-exist) which is on our Public Azure DevOps site. + ## [1.0.202224.23] - 2022-06-06 Lots of fixes, tweaks, and v20 changeover. New Submodule option for licensing. diff --git a/Translations/Spare Brained Licensing Public.g.xlf b/Translations/Spare Brained Licensing Public.g.xlf index d74bfbc..9a4146b 100644 --- a/Translations/Spare Brained Licensing Public.g.xlf +++ b/Translations/Spare Brained Licensing Public.g.xlf @@ -38,6 +38,11 @@ Table SPBPL Extension License - Field Entry Id - Property Caption + + Extension App Id + + Table SPBPL Extension License - Field Extension App Id - Property Caption + Extension Name @@ -58,6 +63,11 @@ Table SPBPL Extension License - Field License Platform - Property Caption + + Licensing ID + + Table SPBPL Extension License - Field Licensing ID - Property Caption + Product Code @@ -68,6 +78,16 @@ Table SPBPL Extension License - Field Product URL - Property Caption + + Sandbox Grace Days + + Table SPBPL Extension License - Field Sandbox Grace Days - Property Caption + + + Submodule Name + + Table SPBPL Extension License - Field Submodule Name - Property Caption + Subscription Cancelled At @@ -118,36 +138,76 @@ Table SPBPL Extension License - Field Version Check URL - Property Caption - - No Subscription was found in the Subscriptions list for AppId: %1 - %1 is which Application ID - Codeunit SPBPL Extension Registration - Method CheckIfActive - NamedType NoSubFoundErr + + An error occured validating the license. Contact %1 for assistance + %1 is the App Publisher + Codeunit SPBPL Activate Meth - Method CheckPlatformCanActivate - NamedType ActivationFailureErr + + + There are no remaining uses of that license key to assign to this installation. + + Codeunit SPBPL Activate Meth - Method CheckPlatformCanActivate - NamedType NoRemainingUsesErr + + + The License Key provided has already expired due to a Subscription End. Contact %1 for assistance + %1 is the App Publisher + Codeunit SPBPL Activate Meth - Method DoActivationInLocalSystem - NamedType LicenseKeyExpiredErr + + + No License was found in the Licenses list for SubscriptionId: %1 + %1 is the ID of the App. + Codeunit SPBPL Check Active - Method CheckBasic - NamedType NoSubFoundErr + + + No License was found in the Licenses list for SubscriptionId: %1 with Submodule name: %2 + %1 is the ID of the App. %2 is the Submodule. + Codeunit SPBPL Check Active - Method CheckBasicSubmodule - NamedType NoSubscriptionFoundErr + + + The License for %1 is not Active. Contact your system administrator to re-activate it. + %1 is the name of the Extension. + Codeunit SPBPL Check Active - Method DoCheckBasic - NamedType SubscriptionInactiveErr + + + <+%1D> + %1 is the number of days + Codeunit SPBPL Check Active Meth - Method DoCheckIfActive - NamedType DaysGraceTok + + + Today is the last trial day for %1. Please purchase a License Key and Activate the subscription to continue use. + %1 is the name of the Extension + Codeunit SPBPL Check Active Meth - Method DoCheckIfActive - NamedType GraceExpiringMsg - - The Subscription for %1 is not Active. Contact your system administrator to re-activate it. - %1 is the name of the Subscription - Codeunit SPBPL Extension Registration - Method CheckIfActive - NamedType SubscriptionInactiveErr + + There was an issue in contacting the licensing server to deactivate this license. Contact %1 for assistance. + %1 is the App Publisher name + Codeunit SPBPL Deactivate Meth - Method DoDeactivate - NamedType DeactivationProblemErr + + + To install this Extension, you need to update %1 to at least version %2. + %1 is the name of licensing extension, %2 is the version number + Codeunit SPBPL Extension Registration - Method CheckSupportedVersion - NamedType VersionUpdateRequiredErr <+%1D> %1 is the number of days Codeunit SPBPL Extension Registration - Method RegisterExtension - NamedType PlusDaysTok - - https://api.gumroad.com/v2/licenses/verify?product_permalink=%1&license_key=%2&increment_uses_count=%3 - %1 %2 %3 - Codeunit SPBPL Gumroad Communicator - NamedType GumroadVerifyAPITok - - - Unable to verify or activate license.\ %1: %2 \ %3 - %1 %2 %3 - Codeunit SPBPL Gumroad Communicator - Method CallAPIForVerification - NamedType WebCallErr + + The key will look like XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX. + + Codeunit SPBPL Gumroad Communicator - NamedType GumroadKeyFormatTok Unable to communicate with the license server due to an environment block. Please resolve and try again. Codeunit SPBPL Gumroad Communicator - Method CallAPIForVerification - NamedType EnvironmentBlockErr + + Unable to verify or activate license.\ %1: %2 \ %3 + %1 %2 %3 + Codeunit SPBPL Gumroad Communicator - Method CallAPIForVerification - NamedType WebCallErr + An error occured validating the license. Contact %1 for assistance %1 is the App Publisher @@ -158,75 +218,45 @@ %1 is the App Publisher Codeunit SPBPL Gumroad Communicator - Method PopulateSubscriptionFromResponse - NamedType ActivationFailureErr - - The key will look like XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX. + + Unable to communicate with the license server due to an environment block. Please resolve and try again. - Codeunit SPBPL Gumroad Communicator - Method SampleKeyFormatText - NamedType GumroadKeyFormatTok - - - %1-%2 - %1 %2 - Codeunit SPBPL IsoStore Manager - NamedType NameMapTok + Codeunit SPBPL LemonSqueezy Comm. - Method CallLemonSqueezy - NamedType EnvironmentBlockErr - - An error occured validating the license. Contact %1 for assistance - %1 is the App Publisher - Codeunit SPBPL License Management - Method ActivateExtension - NamedType ActivationFailureErr + + Unable to verify or activate license.\ %1: %2 \ %3 + %1 %2 %3 + Codeunit SPBPL LemonSqueezy Comm. - Method CallLemonSqueezy - NamedType WebCallErr - - The License Key provided has already expired due to a Subscription End. Contact %1 for assistance + + An error occured communicating with the licensing platform. Contact %1 for assistance %1 is the App Publisher - Codeunit SPBPL License Management - Method ActivateExtension - NamedType LicenseKeyExpiredErr + Codeunit SPBPL LemonSqueezy Comm. - Method PopulateSubscriptionFromResponse - NamedType CommunicationFailureErr - - There are no remaining uses of that license key to assign to this installation. + + The key will look like XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX. - Codeunit SPBPL License Management - Method ActivateFromWizard - NamedType NoRemainingUsesErr - - - Today is the last trial day for %1. Please purchase a License Key and Activate the subscription to continue use. - %1 is the name of the Extension - Codeunit SPBPL License Management - Method CheckIfActive - NamedType GraceExpiringMsg - - - <+%1D> - %1 is the number of days - Codeunit SPBPL License Management - Method CheckIfActive - NamedType DaysGraceTok + Codeunit SPBPL LemonSqueezy Comm. - Method SampleKeyFormatText - NamedType LemonSqueezyKeyFormatTok - - To install this Extension, you need to update %1 to at least version %2. - %1 is the name of extension, %2 is the version number - Codeunit SPBPL License Management - Method CheckSupportedVersion - NamedType VersionUpdateRequiredErr - - - This will deactivate this license in this Business Central instance, but you will need to contact the Publisher to release the assigned license. \ \Are you sure you want to deactivate this license? + + b08c8cbe-ff20-4c38-9448-21e68b509e84 - Codeunit SPBPL License Management - Method DeactivateExtension - NamedType DeactivationWarningQst + Codeunit SPBPL Licensing Install - NamedType GumroadTestSubscriptionIdTok - - Update Extension: %1 - %1 is Extension Name - Codeunit SPBPL License Management - Method DoVersionCheck - NamedType SubjectTok - - - https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/tenant-admin-center-manage-apps#get-an-overview-and-check-for-updates + + 62922d07-87e2-4959-aece-2cacf9222e9b - Codeunit SPBPL License Management - Method DoVersionCheck - NamedType DocsTok + Codeunit SPBPL Licensing Install - NamedType LemonSqueezyTestSubscriptionIdTok - - bwdCu - - Codeunit SPBPL License Utilities - NamedType SelfTestProductIdTok + + %1 Test Subscription + %1 is the Licensing Extension name. + Codeunit SPBPL Licensing Install - Method AddTestProduct - NamedType TestLicenseNameTok - - 21E2339D-F24D4A92-9813B4F2-8ABA083C - - Codeunit SPBPL License Utilities - NamedType SelfTestProductKeyTok - - - Content-Disposition: form-data; name="%1" - %1 is the name of the Form Data - Codeunit SPBPL License Utilities - Method AddNameValuePair - NamedType FormDataNameTok + + Update Extension: %1 + %1 is Extension Name + Codeunit SPBPL Version Check - Method DoVersionCheck - NamedType SubjectTok Extension Licenses @@ -238,6 +268,16 @@ Page SPBPL Extension Licenses - NamedType UpdateAvailableTok + + This will deactivate this license in this Business Central instance, but you will need to contact the Publisher to release the assigned license. \ \Are you sure you want to deactivate this license? + + Page SPBPL Extension Licenses - Method DeactivateExtension - NamedType DeactivationNotPossibleWarningQst + + + This will deactivate this license in this Business Central instance.\ \Are you sure you want to deactivate this license? + + Page SPBPL Extension Licenses - Method DeactivateExtension - NamedType DeactivationPossibleQst + Shows if this Extension has been Activated with a Product Key. @@ -249,20 +289,35 @@ Page SPBPL Extension Licenses - Control Billing Support Email - Property ToolTip - Specifies the value of the Entry Id field. + This Guid is the Subscription Entry Id. Page SPBPL Extension Licenses - Control Entry Id - Property ToolTip + + This Guid is the Extension's App Id. + + Page SPBPL Extension Licenses - Control Extension App Id - Property ToolTip + The name of the Extension that is registered to have a Subscription requirement. Page SPBPL Extension Licenses - Control Extension Name - Property ToolTip + + Specifies the value of the License Platform field. + + Page SPBPL Extension Licenses - Control License Platform - Property ToolTip + The page where one can find more information about purchasing a Subscription for this Extension. Page SPBPL Extension Licenses - Control Product URL - Property ToolTip + + If this Extension uses Module based Subscriptions, this displays which Submodule/Edition this is. + + Page SPBPL Extension Licenses - Control Submodule Name - Property ToolTip + This shows the email address that the License Key is registered to, in case there is a need to find it later. @@ -398,6 +453,11 @@ Enum SPBPL License Platform - EnumValue Gumroad - Property Caption + + LemonSqueezy + + Enum SPBPL License Platform - EnumValue LemonSqueezy - Property Caption + Spare Brained Licensing Admin diff --git a/app.json b/app.json index 2646a72..f0a96e0 100644 --- a/app.json +++ b/app.json @@ -4,7 +4,7 @@ "publisher": "Spare Brained Ideas AB", "brief": "This library helps other Extensions verify a current subscription license.", "description": "This library helps other Extensions verify a current subscription license.", - "version": "1.1.0.0", + "version": "1.2.0.0", "privacyStatement": "https://sparebrained.com/appsource/privacystatement", "EULA": "https://sparebrained.com/appsource/eula", "help": "https://sparebrained.com/support", @@ -18,7 +18,7 @@ "idRanges": [ { "from": 71033, - "to": 71043 + "to": 71049 } ], "resourceExposurePolicy": { @@ -27,7 +27,7 @@ "includeSourceInSymbolFile": true }, "target": "Cloud", - "runtime": "8.0", + "runtime": "10.0", "features": [ "TranslationFile", "NoImplicitWith" ], diff --git a/ruleset.json b/ruleset.json deleted file mode 100644 index b1404da..0000000 --- a/ruleset.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "Project ruleset", - "description": "A list of company specific rules", - "rules": [ - { - "id": "PTE0001", - "action": "None", - "justification": " has an ID of [#######]. It must be between 50000 and 99999. Need to clarify licensing issues with VAR ID range" - }, - { - "id": "PTE0002", - "action": "None", - "justification": " has an ID of [#######]. It must be between 50000 and 99999. Need to clarify licensing issues with VAR ID range" - }, - { - "id": "AS0084", - "action": "None", - "justification": "Not Applicable - The ID range '[50000..99999]' is not valid. It must be within the range allocated to the partner for AppSource, within the range '[1000000..75999999]' allocated to AppSource applications, and outside the range '[50000..99999]' allocated to per-tenant customizations." - } - ] -} \ No newline at end of file diff --git a/src/.obsoleting/SPBPLLicenseManagement.Codeunit.al b/src/.obsoleting/SPBPLLicenseManagement.Codeunit.al new file mode 100644 index 0000000..bcc1a46 --- /dev/null +++ b/src/.obsoleting/SPBPLLicenseManagement.Codeunit.al @@ -0,0 +1,48 @@ +codeunit 71036 "SPBPL License Management" +{ + Permissions = tabledata "SPBPL Extension License" = RIM; + ObsoleteState = Pending; + ObsoleteReason = 'Refactored to new Method Codeunits and separate Event wrapper.'; + + [Obsolete('Use new Events in SPBPL Events codeunit.')] + [IntegrationEvent(false, false)] + local procedure OnAfterLicenseDeactivated(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + end; + + [Obsolete('Use new Events in SPBPL Events codeunit.')] + [IntegrationEvent(false, false)] + local procedure OnAfterLicenseDeactivatedByPlatform(var SPBExtensionLicense: Record "SPBPL Extension License"; ResponseBody: Text) + begin + end; + + [Obsolete('Use new Events in SPBPL Events codeunit.')] + [IntegrationEvent(false, false)] + local procedure OnAfterActivationFailure(var SPBExtensionLicense: Record "SPBPL Extension License"; var AppInfo: ModuleInfo) + begin + end; + + [Obsolete('Use new Events in SPBPL Events codeunit.')] + [IntegrationEvent(false, false)] + local procedure OnBeforeVersionCheckUpgradeAvailable(var SPBExtensionLicense: Record "SPBPL Extension License"; var LatestVersion: Version; var IsHandled: Boolean) + begin + end; + + [Obsolete('Use new Events in SPBPL Events codeunit.')] + [IntegrationEvent(false, false)] + local procedure OnAfterActivationSuccess(var SPBExtensionLicense: Record "SPBPL Extension License"; var AppInfo: ModuleInfo) + begin + end; + + [Obsolete('Use new Events in SPBPL Events codeunit.')] + [IntegrationEvent(false, false)] + local procedure OnAfterVersionCheckFailure(var SPBExtensionLicense: Record "SPBPL Extension License"; var ApiHttpRespMessage: HttpResponseMessage) + begin + end; + + [Obsolete('Use new Events in SPBPL Events codeunit.')] + [IntegrationEvent(false, false)] + internal procedure OnAfterThrowPossibleMisuse(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + end; +} diff --git a/src/.permissions/SPBPLLicensing.PermissionSet.al b/src/.permissions/SPBPLLicensing.PermissionSet.al new file mode 100644 index 0000000..e28dd2d --- /dev/null +++ b/src/.permissions/SPBPLLicensing.PermissionSet.al @@ -0,0 +1,23 @@ +permissionset 71033 "SPBPL Licensing" +{ + Assignable = true; + Caption = 'Spare Brained Licensing Admin'; + Permissions = table "SPBPL Extension License" = X, + tabledata "SPBPL Extension License" = RMI, + codeunit "SPBPL Activate Meth" = X, + codeunit "SPBPL Check Active" = X, + codeunit "SPBPL Check Active Meth" = X, + codeunit "SPBPL Deactivate Meth" = X, + codeunit "SPBPL Environment Watcher" = X, + codeunit "SPBPL Events" = X, + codeunit "SPBPL Extension Registration" = X, + codeunit "SPBPL Gumroad Communicator" = X, + codeunit "SPBPL IsoStore Manager" = X, + codeunit "SPBPL LemonSqueezy Comm." = X, + codeunit "SPBPL License Utilities" = X, + codeunit "SPBPL Licensing Install" = X, + codeunit "SPBPL Upgrade" = X, + codeunit "SPBPL Version Check" = X, + page "SPBPL Extension Licenses" = X, + page "SPBPL License Activation" = X; +} \ No newline at end of file diff --git a/src/.permissions/SPBPLLicensingRO.PermissionSet.al b/src/.permissions/SPBPLLicensingRO.PermissionSet.al new file mode 100644 index 0000000..58e7363 --- /dev/null +++ b/src/.permissions/SPBPLLicensingRO.PermissionSet.al @@ -0,0 +1,23 @@ +permissionset 71034 "SPBPL Licensing RO" +{ + Assignable = true; + Caption = 'SPBPL Licensing RO'; + Permissions = table "SPBPL Extension License" = X, + tabledata "SPBPL Extension License" = RMI, + codeunit "SPBPL Activate Meth" = X, + codeunit "SPBPL Check Active" = X, + codeunit "SPBPL Check Active Meth" = X, + codeunit "SPBPL Deactivate Meth" = X, + codeunit "SPBPL Environment Watcher" = X, + codeunit "SPBPL Events" = X, + codeunit "SPBPL Extension Registration" = X, + codeunit "SPBPL Gumroad Communicator" = X, + codeunit "SPBPL IsoStore Manager" = X, + codeunit "SPBPL LemonSqueezy Comm." = X, + codeunit "SPBPL License Utilities" = X, + codeunit "SPBPL Licensing Install" = X, + codeunit "SPBPL Upgrade" = X, + codeunit "SPBPL Version Check" = X, + page "SPBPL Extension Licenses" = X, + page "SPBPL License Activation" = X; +} diff --git a/src/.rulesets/SPBLicensing.ruleset.json b/src/.rulesets/SPBLicensing.ruleset.json new file mode 100644 index 0000000..11699d7 --- /dev/null +++ b/src/.rulesets/SPBLicensing.ruleset.json @@ -0,0 +1,36 @@ +{ + "name": "Licensing ruleset", + "description": "The ruleset to use for SBI Licensing", + "rules": [ + { + "id": "AA0240", + "action": "None", + "justification": "There are intentionally included email addresses for support with this extension." + }, + { + "id": "AA0072", + "action": "None", + "justification": "I need to use more generic variable names for easing cloning to a PTE." + }, + { + "id": "PTE0001", + "action": "None", + "justification": " has an ID of [#######]. It must be between 50000 and 99999. Need to clarify licensing issues with VAR ID range" + }, + { + "id": "PTE0002", + "action": "None", + "justification": " has an ID of [#######]. It must be between 50000 and 99999. Need to clarify licensing issues with VAR ID range" + }, + { + "id": "AS0084", + "action": "None", + "justification": "Not Applicable - The ID range '[50000..99999]' is not valid. It must be within the range allocated to the partner for AppSource, within the range '[1000000..75999999]' allocated to AppSource applications, and outside the range '[50000..99999]' allocated to per-tenant customizations." + }, + { + "id": "AS0092", + "action": "None", + "justification": "For the public version, I'm not releasing my app insights key. You're welcome to include your own, of course, as telemetry was added in v1.2.0.0." + } + ] +} \ No newline at end of file diff --git a/src/Callables/SPBPLCheckActive.Codeunit.al b/src/Callables/SPBPLCheckActive.Codeunit.al new file mode 100644 index 0000000..02b134a --- /dev/null +++ b/src/Callables/SPBPLCheckActive.Codeunit.al @@ -0,0 +1,61 @@ +/// +/// This Codeunit is for checking basic active/inactive functions to be used by 3rd parties wanting to validate +/// if a license is active. Two main options exist at this time - with or without Submodule functionality. +/// +codeunit 71042 "SPBPL Check Active" +{ + /// + /// This function takes an App ID and checks if it is active or not, along with if the user should be shown errors if Inactive. + /// + /// This should be the App ID + /// If the extension is Inactive, should the user be shown an error? + /// + procedure CheckBasic(SubscriptionId: Guid; InactiveShowError: Boolean) IsActive: Boolean + var + SPBExtensionLicense: Record "SPBPL Extension License"; + NoSubFoundErr: Label 'No License was found in the Licenses list for SubscriptionId: %1', Comment = '%1 is the ID of the App.'; + begin + SPBExtensionLicense.SetRange("Extension App Id"); + //If using this function signature, the Submodule functionality should NOT be considered. + SPBExtensionLicense.SetRange("Submodule Name", ''); + if not SPBExtensionLicense.FindFirst() then + if GuiAllowed() then + Error(NoSubFoundErr, SubscriptionId); + + IsActive := DoCheckBasic(SPBExtensionLicense, InactiveShowError); + end; + + /// + /// This function takes an App ID and Submodule Name and checks if it is active or not, along with if the user should be shown errors if Inactive. + /// + /// This should be the App ID + /// This should be the submodule to check for + /// If the extension is Inactive, should the user be shown an error? + /// + procedure CheckBasicSubmodule(SubscriptionId: Guid; SubmoduleName: Text[100]; InactiveShowError: Boolean) IsActive: Boolean + var + SPBExtensionLicense: Record "SPBPL Extension License"; + NoSubscriptionFoundErr: Label 'No License was found in the Licenses list for SubscriptionId: %1 with Submodule name: %2', Comment = '%1 is the ID of the App. %2 is the Submodule.'; + begin + SPBExtensionLicense.SetRange("Extension App Id"); + SPBExtensionLicense.SetRange("Submodule Name", SubmoduleName); + if not SPBExtensionLicense.FindFirst() then + if GuiAllowed() then + Error(NoSubscriptionFoundErr, SubscriptionId, SubmoduleName); + + IsActive := DoCheckBasic(SPBExtensionLicense, InactiveShowError); + end; + + local procedure DoCheckBasic(var SPBExtensionLicense: Record "SPBPL Extension License"; InactiveShowError: Boolean): Boolean + var + SPBPLCheckActiveMeth: Codeunit "SPBPL Check Active Meth"; + IsActive: Boolean; + SubscriptionInactiveErr: Label 'The License for %1 is not Active. Contact your system administrator to re-activate it.', Comment = '%1 is the name of the Extension.'; + begin + IsActive := SPBPLCheckActiveMeth.CheckIfActive(SPBExtensionLicense); + if not IsActive and InactiveShowError then + if GuiAllowed() then + Error(SubscriptionInactiveErr, SPBExtensionLicense."Extension Name"); + exit(IsActive); + end; +} diff --git a/src/codeunit/SPBLICExtensionRegistration.Codeunit.al b/src/Callables/SPBPLExtensionRegistration.Codeunit.al similarity index 50% rename from src/codeunit/SPBLICExtensionRegistration.Codeunit.al rename to src/Callables/SPBPLExtensionRegistration.Codeunit.al index 0c91c37..19e756f 100644 --- a/src/codeunit/SPBLICExtensionRegistration.Codeunit.al +++ b/src/Callables/SPBPLExtensionRegistration.Codeunit.al @@ -1,6 +1,25 @@ +/// +/// This codeunit is for properly registering Extensions into the Licensing system. +/// codeunit 71034 "SPBPL Extension Registration" { Permissions = tabledata "SPBPL Extension License" = RIM; + + /// + /// This function with an appalling number of parameters allows Extensions to register as License. + /// + /// The ModuleInfo of the Extension + /// The unique Product Code relevant for the Licensing Platform. + /// The unique Product URL for the Licensing Platform. + /// A URL for users looking for support info. This should be a page on your site with product information. + /// An email to contact about any invoicing/payment questions. + /// An optional URL to check for a version string (x.x.x.x). + /// An optional URL to check for update news information. + /// In an On-Premises or Cloud Production environment, how many days may an extension work before requiring activation. + /// In a Cloud Sandbox environment, how many days may an extension work before requiring activation. + /// Since the licensing app has versions too, what minimum version of the Licensing App is required + /// Which platform should be used. Out of box options are Gumroad and LemonSqueezy, though it may be extended. + /// This setting forces changes over any existing record in the Licensing app to ensure new information updates are pushed in. procedure RegisterExtension( AppInfo: ModuleInfo; newProductCode: Text[100]; @@ -31,6 +50,23 @@ codeunit 71034 "SPBPL Extension Registration" forceUpdate); end; + /// + /// This function with an appalling number of parameters allows Extensions to register as License, this one using an optional Submodule functionality, allowing licensing parts of an Extension, or more complex scenarios. + /// + /// The ModuleInfo of the Extension + /// Unique GUID for the Submodule, so you can future-proof against name changes + /// The Display name of the submodule + /// The unique Product Code relevant for the Licensing Platform. + /// The unique Product URL for the Licensing Platform. + /// A URL for users looking for support info. This should be a page on your site with product information. + /// An email to contact about any invoicing/payment questions. + /// An optional URL to check for a version string (x.x.x.x). + /// An optional URL to check for update news information. + /// In an On-Premises or Cloud Production environment, how many days may an extension work before requiring activation. + /// In a Cloud Sandbox environment, how many days may an extension work before requiring activation. + /// Since the licensing app has versions too, what minimum version of the Licensing App is required + /// Which platform should be used. Out of box options are Gumroad and LemonSqueezy, though it may be extended. + /// This setting forces changes over any existing record in the Licensing app to ensure new information updates are pushed in. procedure RegisterExtension( AppInfo: ModuleInfo; SubModuleId: Guid; @@ -48,15 +84,15 @@ codeunit 71034 "SPBPL Extension Registration" forceUpdate: Boolean) var SPBExtensionLicense: Record "SPBPL Extension License"; - SPBLicenseManagement: Codeunit "SPBPL License Management"; - SPBIsoStoreManager: Codeunit "SPBPL IsoStore Manager"; EnvironmentInformation: Codeunit "Environment Information"; - PlusDaysTok: Label '<+%1D>', Comment = '%1 is the number of days '; + SPBIsoStoreManager: Codeunit "SPBPL IsoStore Manager"; + SPBPLTelemetry: Codeunit "SPBPL Telemetry"; GraceEndDate: Date; GraceDays: Integer; + PlusDaysTok: Label '<+%1D>', Comment = '%1 is the number of days '; begin if minimumLicensingAppVersion > Version.Create('1.0.0.0') then - SPBLicenseManagement.CheckSupportedVersion(minimumLicensingAppVersion); + CheckSupportedVersion(minimumLicensingAppVersion); if EnvironmentInformation.IsOnPrem() or EnvironmentInformation.IsProduction() then GraceDays := daysAllowedBeforeActivationProd @@ -81,7 +117,6 @@ codeunit 71034 "SPBPL Extension Registration" SPBExtensionLicense.Modify(); end; end else begin - SPBExtensionLicense."Entry Id" := SubModuleId; SPBExtensionLicense."Submodule Name" := SubModuleName; SPBExtensionLicense."Extension App Id" := AppInfo.Id; @@ -95,27 +130,26 @@ codeunit 71034 "SPBPL Extension Registration" SPBExtensionLicense."Installed At" := CurrentDateTime(); SPBExtensionLicense."Trial Grace End Date" := GraceEndDate; SPBExtensionLicense."Sandbox Grace Days" := daysAllowedBeforeActivationSandbox; - SPBExtensionLicense.Insert(); + SPBExtensionLicense.Insert(true); end; SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'installDate', Format(CurrentDateTime, 0, 9)); SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'preactivationDays', Format(GraceDays)); + SPBPLTelemetry.NewExtensionRegistered(SPBExtensionLicense); end; - procedure CheckIfActive(SubscriptionId: Guid; InactiveShowError: Boolean): Boolean + internal procedure CheckSupportedVersion(minVersion: Version) var - SPBExtensionLicense: Record "SPBPL Extension License"; - SPBLicenseManagement: Codeunit "SPBPL License Management"; - NoSubFoundErr: Label 'No License was found in the Licenses list for SubscriptionId: %1'; - SubscriptionInactiveErr: Label 'The License for %1 is not Active. Contact your system administrator to re-activate it.', Comment = '%1 is the name of the Extension.'; - IsActive: Boolean; + VersionUpdateRequiredErr: Label 'To install this Extension, you need to update %1 to at least version %2.', Comment = '%1 is the name of licensing extension, %2 is the version number'; + AppInfo: ModuleInfo; + begin + NavApp.GetCurrentModuleInfo(AppInfo); + if AppInfo.AppVersion < minVersion then + if GuiAllowed then + Error(VersionUpdateRequiredErr, AppInfo.Name, minVersion); + end; + + [Obsolete('Use new Events in SPBPL Events codeunit.')] + procedure CheckIfActive(SubscriptionId: Guid; InactiveShowError: Boolean): Boolean begin - if not SPBExtensionLicense.get(SubscriptionId) then - if GuiAllowed() then - Error(NoSubFoundErr, SubscriptionId); - IsActive := SPBLicenseManagement.CheckIfActive(SPBExtensionLicense); - if not IsActive and InactiveShowError then - if GuiAllowed() then - Error(SubscriptionInactiveErr, SPBExtensionLicense."Extension Name"); - exit(IsActive); end; } diff --git a/src/EngineLogic/SPBPLActivateMeth.Codeunit.al b/src/EngineLogic/SPBPLActivateMeth.Codeunit.al new file mode 100644 index 0000000..360a739 --- /dev/null +++ b/src/EngineLogic/SPBPLActivateMeth.Codeunit.al @@ -0,0 +1,92 @@ +codeunit 71045 "SPBPL Activate Meth" +{ + Access = Internal; + + internal procedure Activate(var SPBExtensionLicense: Record "SPBPL Extension License") ActivationSuccess: Boolean + var + SPBPLTelemetry: Codeunit "SPBPL Telemetry"; + ResponseBody: Text; + begin + ActivationSuccess := CheckPlatformCanActivate(SPBExtensionLicense, ResponseBody); + if ActivationSuccess then + DoActivationInLocalSystem(SPBExtensionLicense); + + if ActivationSuccess then + SPBPLTelemetry.LicenseActivation(SPBExtensionLicense) + else + SPBPLTelemetry.LicenseActivationFailure(SPBExtensionLicense); + + OnAfterActivate(SPBExtensionLicense); + end; + + local procedure CheckPlatformCanActivate(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text) ActivationSuccess: Boolean + var + LicensePlatform: Interface "SPBPL ILicenseCommunicator"; + ActivationFailureErr: Label 'An error occured validating the license. Contact %1 for assistance', Comment = '%1 is the App Publisher'; + NoRemainingUsesErr: Label 'There are no remaining uses of that license key to assign to this installation.'; + AppInfo: ModuleInfo; + begin + LicensePlatform := SPBExtensionLicense."License Platform"; + // We'll want the App info for events / errors: + NavApp.GetModuleInfo(SPBExtensionLicense."Extension App Id", AppInfo); + + if LicensePlatform.CallAPIForActivation(SPBExtensionLicense, ResponseBody) then begin + if LicensePlatform.ClientSideLicenseCount(SPBExtensionLicense) then begin + if LicensePlatform.CheckAPILicenseCount(SPBExtensionLicense, ResponseBody) then + ActivationSuccess := true + else + if GuiAllowed() then + Error(NoRemainingUsesErr) + else + ActivationSuccess := false; // Yes, default, but being explicit for clarity/future-proofing + end else + // if the Activation is Server Side, then the activation would have failed on a count issue + ActivationSuccess := true; + LicensePlatform.PopulateSubscriptionFromResponse(SPBExtensionLicense, ResponseBody); + end else + // In case of a malformed Implementation where the user is given no errors by the API call CU, we'll have a failsafe one here + if GuiAllowed() then + Error(ActivationFailureErr, AppInfo.Publisher) + else + ActivationSuccess := false; // Yes, default, but being explicit for clarity/future-proofing + end; + + local procedure DoActivationInLocalSystem(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean + var + SPBPLEvents: Codeunit "SPBPL Events"; + SPBPLIsoStoreManager: Codeunit "SPBPL IsoStore Manager"; + LicenseKeyExpiredErr: Label 'The License Key provided has already expired due to a Subscription End. Contact %1 for assistance', Comment = '%1 is the App Publisher'; + AppInfo: ModuleInfo; + begin + // We'll want the App info for events / errors: + NavApp.GetModuleInfo(SPBExtensionLicense."Extension App Id", AppInfo); + + if (SPBExtensionLicense."Subscription End Date" <> 0DT) and + (SPBExtensionLicense."Subscription End Date" < CurrentDateTime) + then begin + SPBExtensionLicense.Activated := false; + SPBExtensionLicense.Modify(); + Commit(); + SPBPLEvents.OnAfterActivationFailure(SPBExtensionLicense, AppInfo); + if GuiAllowed() then + Error(LicenseKeyExpiredErr, AppInfo.Publisher); + end else begin + SPBExtensionLicense.Modify(); + Commit(); + SPBPLEvents.OnAfterActivationSuccess(SPBExtensionLicense, AppInfo); + end; + + // Now pop the details into IsolatedStorage + SPBPLIsoStoreManager.UpdateOrCreateIsoStorage(SPBExtensionLicense); + exit(SPBExtensionLicense.Activated); + end; + + local procedure OnAfterActivate(var SPBExtensionLicense: Record "SPBPL Extension License"); + var + SPBPLEvents: Codeunit "SPBPL Events"; + AppInfo: ModuleInfo; + begin + NavApp.GetModuleInfo(SPBExtensionLicense."Extension App Id", AppInfo); + SPBPLEvents.OnAfterActivationSuccess(SPBExtensionLicense, AppInfo); + end; +} \ No newline at end of file diff --git a/src/EngineLogic/SPBPLCheckActiveMeth.Codeunit.al b/src/EngineLogic/SPBPLCheckActiveMeth.Codeunit.al new file mode 100644 index 0000000..86be157 --- /dev/null +++ b/src/EngineLogic/SPBPLCheckActiveMeth.Codeunit.al @@ -0,0 +1,101 @@ +codeunit 71043 "SPBPL Check Active Meth" +{ + Access = Internal; + + procedure CheckIfActive(var SPBExtensionLicense: Record "SPBPL Extension License") IsActive: Boolean + var + SPBPLDeactivateMeth: Codeunit "SPBPL Deactivate Meth"; + SPBPLEvents: Codeunit "SPBPL Events"; + SPBPLTelemetry: Codeunit "SPBPL Telemetry"; + begin + IsActive := DoCheckIfActive(SPBExtensionLicense); + + // We throw the Event here before we begin to deactivate the subscription. + SPBPLEvents.OnAfterCheckActiveBasic(SPBExtensionLicense, IsActive); + + if IsActive then + SPBPLTelemetry.LicenseCheckSuccess(SPBExtensionLicense) + else + SPBPLTelemetry.LicenseCheckFailure(SPBExtensionLicense); + + if SPBExtensionLicense.Activated and not IsActive then + // If the Check came back FALSE but the Subscription is Active, + // it may be because the subscription has expired or deactivated on the platform. + // We will force the local installation to inactive. + SPBPLDeactivateMeth.Deactivate(SPBExtensionLicense, true); + end; + + procedure DoCheckIfActive(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean + var + EnvironmentInformation: Codeunit "Environment Information"; + SPBIsoStoreManager: Codeunit "SPBPL IsoStore Manager"; + SPBPLTelemetry: Codeunit "SPBPL Telemetry"; + SPBPLVersionCheck: Codeunit "SPBPL Version Check"; + IsoActive: Boolean; + GraceEndDate: Date; + InstallDateTime: DateTime; + IsoDatetime: DateTime; + LastCheckDateTime: DateTime; + IsoNumber: Integer; + LicensePlatform: Interface "SPBPL ILicenseCommunicator"; + DaysGraceTok: Label '<+%1D>', Comment = '%1 is the number of days'; + GraceExpiringMsg: Label 'Today is the last trial day for %1. Please purchase a License Key and Activate the subscription to continue use.', Comment = '%1 is the name of the Extension'; + ResponseBody: Text; + begin + LicensePlatform := SPBExtensionLicense."License Platform"; + + // if the subscription isn't active, check if we're in the 'grace' preinstall window, which always includes the first day of use + if not SPBExtensionLicense.Activated then begin + Evaluate(InstallDateTime, SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'installDate')); + Evaluate(IsoNumber, SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'preactivationDays')); + if IsoNumber > 0 then + GraceEndDate := CalcDate(StrSubstNo(DaysGraceTok, IsoNumber), DT2Date(InstallDateTime)) + else + if (EnvironmentInformation.IsSandbox() and (IsoNumber < 0)) then + // -1 days grace for a Sandbox means it's unlimited use in sandboxes, even if not activated. + exit(true) + else + GraceEndDate := Today; + if (GraceEndDate = Today) and GuiAllowed then + Message(GraceExpiringMsg, SPBExtensionLicense."Extension Name"); + exit(GraceEndDate > Today); + end; + + Evaluate(LastCheckDateTime, SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'lastCheckDate')); + if ((Today() - DT2Date(LastCheckDateTime)) > 0) then begin + if LicensePlatform.CallAPIForVerification(SPBExtensionLicense, ResponseBody, false) then begin + // This may update the End Dates - note: may or may not call .Modify + LicensePlatform.PopulateSubscriptionFromResponse(SPBExtensionLicense, ResponseBody); + SPBExtensionLicense.Modify(); + end; + SPBPLVersionCheck.DoVersionCheck(SPBExtensionLicense); + SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'lastCheckDate', Format(CurrentDateTime, 0, 9)); + end; + + // if the subscription ran out + if (SPBExtensionLicense."Subscription End Date" < CurrentDateTime) and + (SPBExtensionLicense."Subscription End Date" <> 0DT) + then + exit(false); + + + // if the record version IS active, then let's crosscheck against isolated storage + Evaluate(IsoActive, SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'active')); + if not IsoActive then begin + LicensePlatform.ReportPossibleMisuse(SPBExtensionLicense); + SPBPLTelemetry.EventTagMisuseReport(SPBExtensionLicense); + end; + + // Check Record end date against IsoStorage end date + Evaluate(IsoDatetime, SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'endDate')); + if IsoDatetime <> 0DT then + // Only checking at the date level in case of time zone nonsense + if DT2Date(IsoDatetime) <> DT2Date(SPBExtensionLicense."Subscription End Date") then begin + LicensePlatform.ReportPossibleMisuse(SPBExtensionLicense); + SPBPLTelemetry.EventTagMisuseReport(SPBExtensionLicense); + end; + + // Finally, all things checked out + exit(true); + end; +} diff --git a/src/EngineLogic/SPBPLDeactivateMeth.Codeunit.al b/src/EngineLogic/SPBPLDeactivateMeth.Codeunit.al new file mode 100644 index 0000000..f76fa2a --- /dev/null +++ b/src/EngineLogic/SPBPLDeactivateMeth.Codeunit.al @@ -0,0 +1,39 @@ +codeunit 71046 "SPBPL Deactivate Meth" +{ + internal procedure Deactivate(var SPBExtensionLicense: Record "SPBPL Extension License"; ByPlatform: Boolean) DeactivationSuccess: Boolean + begin + DoDeactivate(SPBExtensionLicense, ByPlatform); + end; + + local procedure DoDeactivate(var SPBExtensionLicense: Record "SPBPL Extension License"; ByPlatform: Boolean) DeactivationSuccess: Boolean + var + SPBPLEvents: Codeunit "SPBPL Events"; + SPBPLIsoStoreManager: Codeunit "SPBPL IsoStore Manager"; + SPBPLTelemetry: Codeunit "SPBPL Telemetry"; + LicensePlatform: Interface "SPBPL ILicenseCommunicator"; + DeactivationProblemErr: Label 'There was an issue in contacting the licensing server to deactivate this license. Contact %1 for assistance.', Comment = '%1 is the App Publisher name'; + AppInfo: ModuleInfo; + ResponseBody: Text; + begin + NavApp.GetModuleInfo(SPBExtensionLicense."Extension App Id", AppInfo); + + SPBExtensionLicense.Validate(Activated, false); + SPBExtensionLicense.Modify(); + SPBPLIsoStoreManager.UpdateOrCreateIsoStorage(SPBExtensionLicense); + Commit(); // if calling the API fails, the local should still be marked as deactivated + + if not ByPlatform then begin + LicensePlatform := SPBExtensionLicense."License Platform"; + if not LicensePlatform.CallAPIForDeactivation(SPBExtensionLicense, ResponseBody) then begin + if GuiAllowed() then + Error(DeactivationProblemErr, AppInfo.Publisher); + end else + DeactivationSuccess := true; + SPBPLTelemetry.LicensePlatformDeactivation(SPBExtensionLicense); + SPBPLEvents.OnAfterLicenseDeactivatedByPlatform(SPBExtensionLicense, ResponseBody); + end else begin + SPBPLTelemetry.LicenseDeactivation(SPBExtensionLicense); + SPBPLEvents.OnAfterLicenseDeactivated(SPBExtensionLicense); + end; + end; +} \ No newline at end of file diff --git a/src/EngineLogic/SPBPLVersionCheck.Codeunit.al b/src/EngineLogic/SPBPLVersionCheck.Codeunit.al new file mode 100644 index 0000000..d939d17 --- /dev/null +++ b/src/EngineLogic/SPBPLVersionCheck.Codeunit.al @@ -0,0 +1,56 @@ +codeunit 71044 "SPBPL Version Check" +{ + Access = Internal; + + procedure DoVersionCheck(var SPBExtensionLicense: Record "SPBPL Extension License") + var + UserTask: Record "User Task"; + SPBPLEvents: Codeunit "SPBPL Events"; + SPBPLTelemetry: Codeunit "SPBPL Telemetry"; + IsHandled: Boolean; + ApiHttpClient: HttpClient; + ApiHttpRequestMessage: HttpRequestMessage; + ApiHttpResponseMessage: HttpResponseMessage; + DocsTok: Label 'https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/tenant-admin-center-manage-apps#get-an-overview-and-check-for-updates', Locked = true; + SubjectTok: Label 'Update Extension: %1', Comment = '%1 is Extension Name'; + AppInfo: ModuleInfo; + VersionResponseBody: Text; + LatestVersion: Version; + begin + NavApp.GetCurrentModuleInfo(AppInfo); + if SPBExtensionLicense."Version Check URL" = '' then + exit; + + ApiHttpRequestMessage.SetRequestUri(SPBExtensionLicense."Version Check URL"); + ApiHttpRequestMessage.Method('GET'); + + if ApiHttpClient.Send(ApiHttpRequestMessage, ApiHttpResponseMessage) then begin + if ApiHttpResponseMessage.IsSuccessStatusCode then begin + ApiHttpResponseMessage.Content.ReadAs(VersionResponseBody); + LatestVersion := Version.Create(VersionResponseBody); + if (AppInfo.AppVersion < LatestVersion) then begin + SPBExtensionLicense."Update Available" := true; + SPBExtensionLicense.Modify(); + + SPBPLEvents.OnBeforeVersionCheckUpgradeAvailable(SPBExtensionLicense, LatestVersion, IsHandled); + if IsHandled then + exit; + + UserTask.Init(); + UserTask.Title := StrSubstNo(SubjectTok, AppInfo.Name); + UserTask.SetDescription(DocsTok); + if not IsNullGuid(SPBExtensionLicense."Activated By") then + UserTask."Assigned To" := SPBExtensionLicense."Activated By"; + UserTask."Due DateTime" := CurrentDateTime; + UserTask."Start DateTime" := CurrentDateTime; + UserTask."Object Type" := UserTask."Object Type"::Page; + UserTask."Object ID" := Page::"SPBPL Extension Licenses"; + UserTask.Insert(true); + end; + end else + SPBPLEvents.OnAfterVersionCheckFailure(SPBExtensionLicense, ApiHttpResponseMessage); + end else + SPBPLEvents.OnAfterVersionCheckFailure(SPBExtensionLicense, ApiHttpResponseMessage); + SPBPLTelemetry.VersionUpdateCheck(SPBExtensionLicense); + end; +} diff --git a/src/Extensibility/SPBPLEvents.Codeunit.al b/src/Extensibility/SPBPLEvents.Codeunit.al new file mode 100644 index 0000000..c787a3c --- /dev/null +++ b/src/Extensibility/SPBPLEvents.Codeunit.al @@ -0,0 +1,61 @@ +codeunit 71041 "SPBPL Events" +{ + #region UIEvents + [IntegrationEvent(false, false)] + internal procedure OnBeforeLaunchProductUrl(var SPBExtensionLicense: Record "SPBPL Extension License"; var IsHandled: Boolean) + begin + end; + #endregion UIEvents + + #region ActiveCheckEvents + [IntegrationEvent(false, false)] + internal procedure OnAfterCheckActiveBasic(var SPBExtensionLicense: Record "SPBPL Extension License"; IsActive: Boolean); + begin + end; + #endregion ActiveCheckEvents + + #region VersionEvents + [IntegrationEvent(false, false)] + internal procedure OnBeforeVersionCheckUpgradeAvailable(var SPBExtensionLicense: Record "SPBPL Extension License"; var LatestVersion: Version; var IsHandled: Boolean) + begin + end; + + [IntegrationEvent(false, false)] + internal procedure OnAfterVersionCheckFailure(var SPBExtensionLicense: Record "SPBPL Extension License"; var ApiHttpRespMessage: HttpResponseMessage) + begin + end; + #endregion VersionEvents + + #region MisuseEvents + [IntegrationEvent(false, false)] + internal procedure OnAfterThrowPossibleMisuse(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + end; + #endregion MisuseEvents + + #region Activation + [IntegrationEvent(false, false)] + internal procedure OnAfterActivationSuccess(var SPBExtensionLicense: Record "SPBPL Extension License"; var AppInfo: ModuleInfo) + begin + end; + + [IntegrationEvent(false, false)] + internal procedure OnAfterActivationFailure(var SPBExtensionLicense: Record "SPBPL Extension License"; var AppInfo: ModuleInfo) + begin + end; + #endregion Activation + + + #region DeActivation + [IntegrationEvent(false, false)] + internal procedure OnAfterLicenseDeactivated(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + end; + + [IntegrationEvent(false, false)] + internal procedure OnAfterLicenseDeactivatedByPlatform(var SPBExtensionLicense: Record "SPBPL Extension License"; ResponseBody: Text) + begin + end; + #endregion DeActivation + +} diff --git a/src/Extensibility/SPBPLILicenseCommunicator.Interface.al b/src/Extensibility/SPBPLILicenseCommunicator.Interface.al new file mode 100644 index 0000000..c01acbd --- /dev/null +++ b/src/Extensibility/SPBPLILicenseCommunicator.Interface.al @@ -0,0 +1,31 @@ +interface "SPBPL ILicenseCommunicator" +{ + procedure CallAPIForActivation(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text) ResultOK: Boolean; + + procedure CallAPIForVerification(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text; IncrementLicenseCount: Boolean) ResultOK: Boolean; + + procedure CallAPIForDeactivation(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text) ResultOK: Boolean; + + procedure ReportPossibleMisuse(SPBExtensionLicense: Record "SPBPL Extension License"); + + procedure PopulateSubscriptionFromResponse(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text); + + procedure ClientSideDeactivationPossible(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean; + + procedure ClientSideLicenseCount(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean; + + procedure CheckAPILicenseCount(var SPBExtensionLicense: Record "SPBPL Extension License"; ResponseBody: Text): Boolean + + procedure SampleKeyFormatText(): Text; + + + procedure GetTestProductUrl(): Text; + + procedure GetTestProductId(): Text; + + procedure GetTestProductKey(): Text; + + procedure GetTestSupportUrl(): Text; + + procedure GetTestBillingEmail(): Text; +} diff --git a/src/enum/SPBLICLicensePlatform.Enum.al b/src/Extensibility/SPBPLLicensePlatform.Enum.al similarity index 61% rename from src/enum/SPBLICLicensePlatform.Enum.al rename to src/Extensibility/SPBPLLicensePlatform.Enum.al index a5c7c5f..a649631 100644 --- a/src/enum/SPBLICLicensePlatform.Enum.al +++ b/src/Extensibility/SPBPLLicensePlatform.Enum.al @@ -7,5 +7,9 @@ enum 71033 "SPBPL License Platform" implements "SPBPL ILicenseCommunicator" Caption = 'Gumroad'; Implementation = "SPBPL ILicenseCommunicator" = "SPBPL Gumroad Communicator"; } - + value(1; LemonSqueezy) + { + Caption = 'LemonSqueezy'; + Implementation = "SPBPL ILicenseCommunicator" = "SPBPL LemonSqueezy Comm."; + } } diff --git a/src/InstallUpgradeBC/SPBPLEnvironmentWatcher.Codeunit.al b/src/InstallUpgradeBC/SPBPLEnvironmentWatcher.Codeunit.al new file mode 100644 index 0000000..f51dfa2 --- /dev/null +++ b/src/InstallUpgradeBC/SPBPLEnvironmentWatcher.Codeunit.al @@ -0,0 +1,24 @@ +codeunit 71047 "SPBPL Environment Watcher" +{ + Access = Internal; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Environment Triggers", 'OnAfterCopyEnvironmentPerDatabase', '', false, false)] + local procedure DeactivateLicensesWhenEnvironmentCopied(DestinationEnvironmentType: Option Production,Sandbox) + var + SPBExtensionLicense: Record "SPBPL Extension License"; + SPBPLDeactivateMeth: Codeunit "SPBPL Deactivate Meth"; + GraceDaysMathTok: Label '<+%1D>', Locked = true; + begin + if SPBExtensionLicense.FindSet(true) then + repeat + if DestinationEnvironmentType = DestinationEnvironmentType::Sandbox then begin + // Reset all licenses to Sandbox grace + if SPBExtensionLicense."Sandbox Grace Days" <> 0 then + SPBExtensionLicense."Trial Grace End Date" := CalcDate(StrSubstNo(GraceDaysMathTok, SPBExtensionLicense."Sandbox Grace Days"), Today); + SPBPLDeactivateMeth.Deactivate(SPBExtensionLicense, false); + end else + // Deactive the licenses in general + SPBPLDeactivateMeth.Deactivate(SPBExtensionLicense, false); + until SPBExtensionLicense.Next() = 0; + end; +} diff --git a/src/InstallUpgradeBC/SPBPLLicensingInstall.Codeunit.al b/src/InstallUpgradeBC/SPBPLLicensingInstall.Codeunit.al new file mode 100644 index 0000000..0c1f3b3 --- /dev/null +++ b/src/InstallUpgradeBC/SPBPLLicensingInstall.Codeunit.al @@ -0,0 +1,62 @@ +codeunit 71037 "SPBPL Licensing Install" +{ + Subtype = Install; + + var + GumroadTestSubscriptionIdTok: Label 'b08c8cbe-ff20-4c38-9448-21e68b509e84'; + LemonSqueezyTestSubscriptionIdTok: Label '62922d07-87e2-4959-aece-2cacf9222e9b'; + + trigger OnInstallAppPerDatabase() + var + SPBPLTelemetry: Codeunit "SPBPL Telemetry"; + begin + PerformInstallOfTestSubscriptions(); + SPBPLTelemetry.LicensingAppInstalled(); + end; + + procedure PerformInstallOfTestSubscriptions() + begin + AddTestProduct(Enum::"SPBPL License Platform"::Gumroad, GumroadTestSubscriptionIdTok); + AddTestProduct(Enum::"SPBPL License Platform"::LemonSqueezy, LemonSqueezyTestSubscriptionIdTok); + end; + + procedure GetGumroadTestAppId() TestProductGuid: Guid + begin + Evaluate(TestProductGuid, GumroadTestSubscriptionIdTok); + end; + + procedure GetLemongSqueezyTestAppId() TestProductGuid: Guid + begin + Evaluate(TestProductGuid, LemonSqueezyTestSubscriptionIdTok); + end; + + internal procedure AddTestProduct(WhichLicensePlatform: Enum "SPBPL License Platform"; TestProductId: Text) + var + SPBExtensionLicense: Record "SPBPL Extension License"; + TestLicenseNameTok: Label '%1 Test Subscription', Comment = '%1 is the Licensing Extension name.'; + LicensePlatform: Interface "SPBPL ILicenseCommunicator"; + AppInfo: ModuleInfo; + TestProductGuid: Guid; + begin + NavApp.GetCurrentModuleInfo(AppInfo); + Evaluate(TestProductGuid, TestProductId); + + if not SPBExtensionLicense.Get(TestProductGuid) then begin + SPBExtensionLicense.Init(); + Evaluate(SPBExtensionLicense."Entry Id", TestProductGuid); + SPBExtensionLicense.Insert(true); + end; + + SPBExtensionLicense."Extension App Id" := AppInfo.Id; + SPBExtensionLicense."Extension Name" := StrSubstNo(TestLicenseNameTok, AppInfo.Name); + SPBExtensionLicense."License Platform" := WhichLicensePlatform; + LicensePlatform := SPBExtensionLicense."License Platform"; + SPBExtensionLicense."Submodule Name" := CopyStr(UpperCase(Format(WhichLicensePlatform)), 1, MaxStrLen(SPBExtensionLicense."Submodule Name")); + + SPBExtensionLicense."Product Code" := CopyStr(LicensePlatform.GetTestProductId(), 1, MaxStrLen(SPBExtensionLicense."Product Code")); + SPBExtensionLicense."Product URL" := CopyStr(LicensePlatform.GetTestProductUrl(), 1, MaxStrLen(SPBExtensionLicense."Product URL")); + SPBExtensionLicense."Support URL" := CopyStr(LicensePlatform.GetTestSupportUrl(), 1, MaxStrLen(SPBExtensionLicense."Support URL")); + SPBExtensionLicense."Billing Support Email" := CopyStr(LicensePlatform.GetTestBillingEmail(), 1, MaxStrLen(SPBExtensionLicense."Billing Support Email")); + SPBExtensionLicense.Modify(); + end; +} diff --git a/src/InstallUpgradeBC/SPBPLUpgrade.Codeunit.al b/src/InstallUpgradeBC/SPBPLUpgrade.Codeunit.al new file mode 100644 index 0000000..3813e5b --- /dev/null +++ b/src/InstallUpgradeBC/SPBPLUpgrade.Codeunit.al @@ -0,0 +1,61 @@ +codeunit 71039 "SPBPL Upgrade" +{ + Subtype = Upgrade; + + trigger OnUpgradePerCompany() + var + SPBPLTelemetry: Codeunit "SPBPL Telemetry"; + UpgradeTag: Codeunit "Upgrade Tag"; + begin + if not UpgradeTag.HasUpgradeTag(v20ReasonLbl) then begin + Performv20Upgrade(); + UpgradeTag.SetUpgradeTag(v20ReasonLbl); + end; + if not UpgradeTag.HasUpgradeTag(v21ReasonLbl) then begin + Performv21Upgrade(); + UpgradeTag.SetUpgradeTag(v21ReasonLbl); + end; + SPBPLTelemetry.LicensingAppUpgraded(); + end; + + local procedure Performv20Upgrade() + var + SPBExtensionLicense: Record "SPBPL Extension License"; + begin + if SPBExtensionLicense.FindSet() then + repeat + if IsNullGuid(SPBExtensionLicense."Extension App Id") then begin + SPBExtensionLicense."Extension App Id" := SPBExtensionLicense."Entry Id"; + SPBExtensionLicense.Modify(true); + end; + until SPBExtensionLicense.Next() = 0; + end; + + local procedure Performv21Upgrade() + var + SPBExtensionLicense: Record "SPBPL Extension License"; + SPBPLensingInstall: Codeunit "SPBPL Licensing Install"; + AppInfo: ModuleInfo; + begin + // Removing any older Subscriptions that was just for Gumroad + NavApp.GetCurrentModuleInfo(AppInfo); + SPBExtensionLicense.SetRange("Extension App Id", AppInfo.Id); + SPBExtensionLicense.DeleteAll(); + + // To using the submodule system to test whatever platforms + SPBPLensingInstall.PerformInstallOfTestSubscriptions(); + end; + + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", 'OnGetPerCompanyUpgradeTags', '', false, false)] + local procedure OnGetPerCompanyUpgradeTags(var PerCompanyUpgradeTags: List of [Code[250]]); + begin + PerCompanyUpgradeTags.Add(v20ReasonLbl); + PerCompanyUpgradeTags.Add(v21ReasonLbl); + end; + + + var + v20ReasonLbl: Label 'SBI-V20-20220430', Locked = true; + v21ReasonLbl: Label 'SBILicensing-V21-20230212', Locked = true; +} \ No newline at end of file diff --git a/src/codeunit/SPBLICGumroadCommunicator.Codeunit.al b/src/PlatformObjects/Gumroad/SPBPLGumroadCommunicator.Codeunit.al similarity index 74% rename from src/codeunit/SPBLICGumroadCommunicator.Codeunit.al rename to src/PlatformObjects/Gumroad/SPBPLGumroadCommunicator.Codeunit.al index 37d26d9..c57cb71 100644 --- a/src/codeunit/SPBLICGumroadCommunicator.Codeunit.al +++ b/src/PlatformObjects/Gumroad/SPBPLGumroadCommunicator.Codeunit.al @@ -2,19 +2,29 @@ codeunit 71035 "SPBPL Gumroad Communicator" implements "SPBPL ILicenseCommunicat { var + GumroadBillingEmailTok: Label 'support@sparebrained.com', Locked = true; + GumroadKeyFormatTok: Label 'The key will look like XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX.'; + GumroadSupportUrlTok: Label 'support@sparebrained.com', Locked = true; + GumroadTestProductIdTok: Label 'bwdCu', Locked = true; + GumroadTestProductKeyTok: Label '21E2339D-F24D4A92-9813B4F2-8ABA083C', Locked = true; + GumroadTestProductUrlTok: Label 'https://sparebrained.gumroad.com/l/SBILicensingTest', Locked = true; GumroadVerifyAPITok: Label 'https://api.gumroad.com/v2/licenses/verify?product_permalink=%1&license_key=%2&increment_uses_count=%3', Comment = '%1 %2 %3', Locked = true; + procedure CallAPIForActivation(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text) ResultOK: Boolean + begin + exit(CallAPIForVerification(SPBExtensionLicense, ResponseBody, true)); + end; + procedure CallAPIForVerification(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text; IncrementLicenseCount: Boolean) ResultOK: Boolean var NAVAppSetting: Record "NAV App Setting"; - EnvInformation: Codeunit "Environment Information"; ApiHttpClient: HttpClient; ApiHttpRequestMessage: HttpRequestMessage; ApiHttpResponseMessage: HttpResponseMessage; - VerifyAPI: Text; - WebCallErr: Label 'Unable to verify or activate license.\ %1: %2 \ %3', Comment = '%1 %2 %3'; EnvironmentBlockErr: Label 'Unable to communicate with the license server due to an environment block. Please resolve and try again.'; + WebCallErr: Label 'Unable to verify or activate license.\ %1: %2 \ %3', Comment = '%1 %2 %3'; AppInfo: ModuleInfo; + VerifyAPI: Text; begin // We REQUIRE HTTP access, so we'll force it on, regardless of Sandbox NavApp.GetCurrentModuleInfo(AppInfo); @@ -49,12 +59,17 @@ codeunit 71035 "SPBPL Gumroad Communicator" implements "SPBPL ILicenseCommunicat Error(WebCallErr, ApiHttpResponseMessage.HttpStatusCode, ApiHttpResponseMessage.ReasonPhrase, ApiHttpResponseMessage.Content); end; + procedure CallAPIForDeactivation(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text) ResultOK: Boolean + begin + exit(CallAPIForVerification(SPBExtensionLicense, ResponseBody, false)); + end; procedure ReportPossibleMisuse(SPBExtensionLicense: Record "SPBPL Extension License") + var + SPBPLEvents: Codeunit "SPBPL Events"; begin // Potential future use of 'reporting' misuse attempts. For example, someone programmatically changing the Subscription Record - - OnAfterThrowPossibleMisuse(SPBExtensionLicense); + SPBPLEvents.OnAfterThrowPossibleMisuse(SPBExtensionLicense); end; #pragma warning disable AA0150 // TODO - Passed as "var" for the interface @@ -62,11 +77,11 @@ codeunit 71035 "SPBPL Gumroad Communicator" implements "SPBPL ILicenseCommunicat #pragma warning restore AA0150 var TempJsonBuffer: Record "JSON Buffer" temporary; - TempPlaceholder: Text; - AppInfo: ModuleInfo; GumroadJson: JsonObject; GumroadToken: JsonToken; ActivationFailureErr: Label 'An error occured validating the license. Contact %1 for assistance', Comment = '%1 is the App Publisher'; + AppInfo: ModuleInfo; + TempPlaceholder: Text; begin NavApp.GetModuleInfo(SPBExtensionLicense."Extension App Id", AppInfo); GumroadJson.ReadFrom(ResponseBody); @@ -80,8 +95,6 @@ codeunit 71035 "SPBPL Gumroad Communicator" implements "SPBPL ILicenseCommunicat // Update the current Subscription record SPBExtensionLicense.Validate(Activated, true); - TempJsonBuffer.GetPropertyValue(TempPlaceholder, 'license_key'); - SPBExtensionLicense."License Key" := CopyStr(TempPlaceholder, 1, MaxStrLen(SPBExtensionLicense."License Key")); TempJsonBuffer.GetPropertyValue(TempPlaceholder, 'created_at'); Evaluate(SPBExtensionLicense."Created At", TempPlaceholder); TempJsonBuffer.GetPropertyValue(TempPlaceholder, 'subscription_ended_at'); @@ -96,19 +109,31 @@ codeunit 71035 "SPBPL Gumroad Communicator" implements "SPBPL ILicenseCommunicat SPBExtensionLicense.CalculateEndDate(); end; + procedure ClientSideDeactivationPossible(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean; + begin + // Gumroad only allows this using an API key, which is unique to each Publisher. At this time, + // I can't support the safe storage of that information + exit(false); + end; + + procedure ClientSideLicenseCount(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean; + begin + exit(true); + end; + procedure CheckAPILicenseCount(var SPBExtensionLicense: Record "SPBPL Extension License"; ResponseBody: Text): Boolean var TempJsonBuffer: Record "JSON Buffer" temporary; - GumroadSPBLicenseUtilities: Codeunit "SPBPL License Utilities"; - LicenseUses: Integer; + SPBPLenseUtilities: Codeunit "SPBPL License Utilities"; LicenseCount: Integer; - AppInfo: ModuleInfo; + LicenseUses: Integer; GumroadJson: JsonObject; GumroadToken: JsonToken; GumroadErr: Label 'An error occured validating the license. Contact %1 for assistance', Comment = '%1 is the App Publisher'; + AppInfo: ModuleInfo; begin // The 'Test' product, we never do a Count check on this application - if SPBExtensionLicense."Entry Id" = GumroadSPBLicenseUtilities.GetTestProductAppId() then + if SPBExtensionLicense."Entry Id" = SPBPLenseUtilities.GetTestProductAppId() then exit(true); GumroadJson.ReadFrom(ResponseBody); @@ -128,12 +153,36 @@ codeunit 71035 "SPBPL Gumroad Communicator" implements "SPBPL ILicenseCommunicat end; procedure SampleKeyFormatText(): Text - var - GumroadKeyFormatTok: Label 'The key will look like XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX.'; begin exit(GumroadKeyFormatTok); end; + procedure GetTestProductUrl(): Text + begin + exit(GumroadTestProductUrlTok); + end; + + procedure GetTestProductId(): Text + begin + exit(GumroadTestProductIdTok); + end; + + procedure GetTestProductKey(): Text + begin + exit(GumroadTestProductKeyTok); + end; + + procedure GetTestSupportUrl(): Text + begin + exit(GumroadSupportUrlTok); + end; + + procedure GetTestBillingEmail(): Text + begin + exit(GumroadBillingEmailTok); + end; + + [Obsolete('This event is moved to the central License Management codeunit for platform-agnostic eventing.')] [IntegrationEvent(false, false)] local procedure OnAfterThrowPossibleMisuse(SPBExtensionLicense: Record "SPBPL Extension License") begin diff --git a/src/PlatformObjects/LemonSqueezy/SPBPLLemonSqueezyComm.Codeunit.al b/src/PlatformObjects/LemonSqueezy/SPBPLLemonSqueezyComm.Codeunit.al new file mode 100644 index 0000000..3c33cc3 --- /dev/null +++ b/src/PlatformObjects/LemonSqueezy/SPBPLLemonSqueezyComm.Codeunit.al @@ -0,0 +1,241 @@ +codeunit 71040 "SPBPL LemonSqueezy Comm." implements "SPBPL ILicenseCommunicator" +{ + var + LemonSqueezyActivateAPITok: Label 'https://api.lemonsqueezy.com/v1/licenses/activate?license_key=%1&instance_name=%2', Comment = '%1 is the license key, %2 is just a label in the Lemon Squeezy list of Licenses', Locked = true; + LemonSqueezyBillingEmailTok: Label 'support@sparebrained.com', Locked = true; + LemonSqueezyDeactivateAPITok: Label 'https://api.lemonsqueezy.com/v1/licenses/deactivate?license_key=%1&instance_id=%2', Comment = '%1 is the license key, %2 is the unique guid assigned by Lemon Squeezy for this installation, created during Activation.', Locked = true; + LemonSqueezySupportUrlTok: Label 'support@sparebrained.com', Locked = true; + LemonSqueezyTestProductIdTok: Label '39128', Locked = true; + LemonSqueezyTestProductKeyTok: Label 'CE2F02DE-657C-4F76-8F93-0E352C9A30B2', Locked = true; + LemonSqueezyTestProductUrlTok: Label 'https://sparebrained.lemonsqueezy.com/checkout/buy/cab72f9c-add0-47b0-9a09-feb3b4ccf8e0', Locked = true; + LemonSqueezyVerifyAPITok: Label 'https://api.gumroad.com/v2/licenses/verify?license_key=%1&instance_id=%2', Comment = '%1 is the license key, %2 is the unique guid assigned by Lemon Squeezy for this installation, created during Activation.', Locked = true; + + procedure CallAPIForActivation(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text): Boolean + var + EnvInformation: Codeunit "Environment Information"; + OnPremEnvironmentIDTok: Label 'OnPrem-%1-%2', Comment = '%1 is the Tenant ID, %2 is the Environment name', Locked = true; + ProdEnvironmentIDTok: Label 'Prod-%1-%2', Comment = '%1 is the Tenant ID, %2 is the Environment name', Locked = true; + SandboxEnvironmentIDTok: Label 'Sandbox-%1-%2', Comment = '%1 is the Tenant ID, %2 is the Environment name', Locked = true; + ActivateAPI: Text; + EnvironID: Text; + begin + // When activating against LemonSqueezy, we want to register the tenant ID in their end, plus environ type + case true of + EnvInformation.IsOnPrem(): + EnvironID := StrSubstNo(OnPremEnvironmentIDTok, Database.TenantId(), EnvInformation.GetEnvironmentName()); + EnvInformation.IsSandbox(): + EnvironID := StrSubstNo(SandboxEnvironmentIDTok, Database.TenantId(), EnvInformation.GetEnvironmentName()); + EnvInformation.IsProduction(): + EnvironID := StrSubstNo(ProdEnvironmentIDTok, Database.TenantId(), EnvInformation.GetEnvironmentName()); + end; + + ActivateAPI := StrSubstNo(LemonSqueezyActivateAPITok, SPBExtensionLicense."License Key", EnvironID); + exit(CallLemonSqueezy(ResponseBody, ActivateAPI)); + end; + + procedure CallAPIForVerification(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text; IncrementLicenseCount: Boolean): Boolean + var + VerifyAPI: Text; + begin + // First, we'll crosscheck that the record's license_id matches the IsoStorage one for possible tamper checking + ValidateLicenseIdInfo(SPBExtensionLicense); + + // When verifying the License, we have to pass the instance info that we stored on the record + VerifyAPI := StrSubstNo(LemonSqueezyVerifyAPITok, SPBExtensionLicense."License Key", SPBExtensionLicense."Licensing ID"); + exit(CallLemonSqueezy(ResponseBody, VerifyAPI)); + end; + + procedure CallAPIForDeactivation(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text) ResultOK: Boolean + var + DeactivateAPI: Text; + begin + // First, we'll crosscheck that the record's license_id matches the IsoStorage one for possible tamper checking + ValidateLicenseIdInfo(SPBExtensionLicense); + + // When verifying the License, we have to pass the instance info that we stored on the record + DeactivateAPI := StrSubstNo(LemonSqueezyDeactivateAPITok, SPBExtensionLicense."License Key", SPBExtensionLicense."Licensing ID"); + exit(CallLemonSqueezy(ResponseBody, DeactivateAPI)); + end; + + local procedure CallLemonSqueezy(var ResponseBody: Text; LemonSquezyRequestUri: Text): Boolean + var + NAVAppSetting: Record "NAV App Setting"; + ApiHttpClient: HttpClient; + ApiHttpRequestMessage: HttpRequestMessage; + ApiHttpResponseMessage: HttpResponseMessage; + EnvironmentBlockErr: Label 'Unable to communicate with the license server due to an environment block. Please resolve and try again.'; + WebCallErr: Label 'Unable to verify or activate license.\ %1: %2 \ %3', Comment = '%1 %2 %3'; + AppInfo: ModuleInfo; + begin + // We REQUIRE HTTP access, so we'll force it on, regardless of Sandbox + NavApp.GetCurrentModuleInfo(AppInfo); + if NAVAppSetting.Get(AppInfo.Id) then begin + if not NAVAppSetting."Allow HttpClient Requests" then begin + NAVAppSetting."Allow HttpClient Requests" := true; + NAVAppSetting.Modify(); + end + end else begin + NAVAppSetting."App ID" := AppInfo.Id; + NAVAppSetting."Allow HttpClient Requests" := true; + NAVAppSetting.Insert(); + end; + + ApiHttpRequestMessage.SetRequestUri(LemonSquezyRequestUri); + ApiHttpRequestMessage.Method('POST'); + + if not ApiHttpClient.Send(ApiHttpRequestMessage, ApiHttpResponseMessage) then begin + if ApiHttpResponseMessage.IsBlockedByEnvironment then begin + if GuiAllowed() then + Error(EnvironmentBlockErr) + end else + if GuiAllowed() then + Error(WebCallErr, ApiHttpResponseMessage.HttpStatusCode, ApiHttpResponseMessage.ReasonPhrase, ApiHttpResponseMessage.Content); + end else + if ApiHttpResponseMessage.IsSuccessStatusCode() then begin + ApiHttpResponseMessage.Content.ReadAs(ResponseBody); + exit(true); + end else + if GuiAllowed() then + Error(WebCallErr, ApiHttpResponseMessage.HttpStatusCode, ApiHttpResponseMessage.ReasonPhrase, ApiHttpResponseMessage.Content); + end; + + local procedure ValidateLicenseIdInfo(var SPBExtensionLicense: Record "SPBPL Extension License") + var + SPBIsoStoreManager: Codeunit "SPBPL IsoStore Manager"; + LSqueezyIdJson: JsonObject; + LSqueezyIdJsonToken: JsonToken; + TempPlaceholder: Text; + begin + TempPlaceholder := SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'licensingId'); + if LSqueezyIdJson.ReadFrom(TempPlaceholder) then + if LSqueezyIdJson.Get('id', LSqueezyIdJsonToken) then + if LSqueezyIdJsonToken.AsValue().AsText() <> SPBExtensionLicense."Licensing ID" then + ReportPossibleMisuse(SPBExtensionLicense); + end; + + procedure ReportPossibleMisuse(SPBExtensionLicense: Record "SPBPL Extension License") + var + SPBPLEvents: Codeunit "SPBPL Events"; + begin + // Potential future use of 'reporting' misuse attempts. For example, someone programmatically changing the Subscription Record + SPBPLEvents.OnAfterThrowPossibleMisuse(SPBExtensionLicense); + end; + +#pragma warning disable AA0150 + //The interface implements this as 'var', so yes, this is fine. + procedure PopulateSubscriptionFromResponse(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text) +#pragma warning restore AA0150 + var + TempJsonBuffer: Record "JSON Buffer" temporary; + SPBIsoStoreManager: Codeunit "SPBPL IsoStore Manager"; + CurrentActiveStatus: Boolean; + InstanceInfo: JsonObject; + LSqueezyJson: JsonObject; + LSqueezyToken: JsonToken; + CommunicationFailureErr: Label 'An error occured communicating with the licensing platform. Contact %1 for assistance', Comment = '%1 is the App Publisher'; + AppInfo: ModuleInfo; + SqueezyReponseType: Option " ",Activation,Validation,Deactivation; + TempPlaceholder: Text; + begin + // This is a generic function to process all Responses, regardless of Activation, Validation, or Deactivation + // Which means we need to detect what mode we're in. + NavApp.GetModuleInfo(SPBExtensionLicense."Extension App Id", AppInfo); + LSqueezyJson.ReadFrom(ResponseBody); + case true of + LSqueezyJson.Get('activated', LSqueezyToken): + begin + SqueezyReponseType := SqueezyReponseType::Activation; + CurrentActiveStatus := LSqueezyToken.AsValue().AsBoolean(); + end; + LSqueezyJson.Get('valid', LSqueezyToken): + begin + SqueezyReponseType := SqueezyReponseType::Validation; + CurrentActiveStatus := LSqueezyToken.AsValue().AsBoolean(); + end; + LSqueezyJson.Get('deactivated', LSqueezyToken): + begin + SqueezyReponseType := SqueezyReponseType::Deactivation; + // We flip. If deactivated is true, then it's not active. + CurrentActiveStatus := not LSqueezyToken.AsValue().AsBoolean(); + end; + end; + if SqueezyReponseType = SqueezyReponseType::" " then + if GuiAllowed() then + Error(CommunicationFailureErr, AppInfo.Publisher); + + TempJsonBuffer.ReadFromText(ResponseBody); + + // Update the current Subscription record + SPBExtensionLicense.Validate(Activated, CurrentActiveStatus); + TempJsonBuffer.GetPropertyValueAtPath(TempPlaceholder, 'created_at', 'license_key'); + Evaluate(SPBExtensionLicense."Created At", TempPlaceholder); + TempJsonBuffer.GetPropertyValueAtPath(TempPlaceholder, 'expires_at', 'license_key'); + Evaluate(SPBExtensionLicense."Subscription Ended At", TempPlaceholder); + + // TODO: Pending Request to Lemon Squeezy team to add this to their API + //TempJsonBuffer.GetPropertyValueAtPath(TempPlaceholder, 'email', 'license_key'); + //SPBExtensionLicense."Subscription Email" := CopyStr(TempPlaceholder, 1, MaxStrLen(SPBExtensionLicense."Subscription Email")); + SPBExtensionLicense.CalculateEndDate(); + + // Lemon Squeezy relies on having storage of the "instance ID" to verify an instance is still active + if SqueezyReponseType = SqueezyReponseType::Activation then begin + TempJsonBuffer.GetPropertyValueAtPath(TempPlaceholder, 'id', '*instance*'); + SPBExtensionLicense."Licensing ID" := CopyStr(TempPlaceholder, 1, MaxStrLen(SPBExtensionLicense."Licensing ID")); + InstanceInfo.Add('id', TempPlaceholder); + TempJsonBuffer.GetPropertyValueAtPath(TempPlaceholder, 'name', '*instance*'); + InstanceInfo.Add('name', TempPlaceholder); + + InstanceInfo.WriteTo(TempPlaceholder); + SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'licensingId', SPBExtensionLicense."Licensing ID"); + end; + end; + + procedure ClientSideDeactivationPossible(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean; + begin + // LemonSqueezy allows self-unregistration of an instance of a license + exit(true); + end; + + procedure ClientSideLicenseCount(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean; + begin + exit(false); + end; + + procedure CheckAPILicenseCount(var SPBExtensionLicense: Record "SPBPL Extension License"; ResponseBody: Text): Boolean + begin + // LemonSqueezy does server side count checking during the Activation flow, so we should NOT check client side. + exit(true); + end; + + procedure SampleKeyFormatText(): Text + var + LemonSqueezyKeyFormatTok: Label 'The key will look like XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.'; + begin + exit(LemonSqueezyKeyFormatTok); + end; + + procedure GetTestProductUrl(): Text + begin + exit(LemonSqueezyTestProductUrlTok); + end; + + procedure GetTestProductId(): Text + begin + exit(LemonSqueezyTestProductIdTok); + end; + + procedure GetTestProductKey(): Text + begin + exit(LemonSqueezyTestProductKeyTok); + end; + + procedure GetTestSupportUrl(): Text + begin + exit(LemonSqueezySupportUrlTok); + end; + + procedure GetTestBillingEmail(): Text + begin + exit(LemonSqueezyBillingEmailTok); + end; +} diff --git a/src/SPBPLLicenseUtilities.Codeunit.al b/src/SPBPLLicenseUtilities.Codeunit.al new file mode 100644 index 0000000..260d691 --- /dev/null +++ b/src/SPBPLLicenseUtilities.Codeunit.al @@ -0,0 +1,18 @@ +codeunit 71033 "SPBPL License Utilities" +{ + internal procedure GetTestProductAppId(): Guid + var + AppInfo: ModuleInfo; + begin + NavApp.GetCurrentModuleInfo(AppInfo); + exit(AppInfo.Id); + end; + + internal procedure GetTestProductKey(SPBExtensionLicense: Record "SPBPL Extension License"): Text + var + LicensePlatform: Interface "SPBPL ILicenseCommunicator"; + begin + LicensePlatform := SPBExtensionLicense."License Platform"; + exit(LicensePlatform.GetTestProductKey()); + end; +} diff --git a/src/table/SPBLICExtensionLicense.Table.al b/src/Storage/SPBPLExtensionLicense.Table.al similarity index 90% rename from src/table/SPBLICExtensionLicense.Table.al rename to src/Storage/SPBPLExtensionLicense.Table.al index b912aab..9b14782 100644 --- a/src/table/SPBLICExtensionLicense.Table.al +++ b/src/Storage/SPBPLExtensionLicense.Table.al @@ -151,8 +151,8 @@ table 71033 "SPBPL Extension License" field(26; "Update Available"; Boolean) { Caption = 'Update Available'; - Editable = false; DataClassification = SystemMetadata; + Editable = false; } field(27; "Update News URL"; Text[250]) { @@ -173,6 +173,13 @@ table 71033 "SPBPL Extension License" DataClassification = SystemMetadata; Editable = false; } + field(40; "Licensing ID"; Text[250]) + { + // This is for generic storage of 3rd party system ID fields in case needed + Caption = 'Licensing ID'; + DataClassification = CustomerContent; + Editable = false; + } } keys { @@ -210,8 +217,18 @@ table 71033 "SPBPL Extension License" internal procedure IsTestSubscription(): Boolean var - SPBPLLicenseUtilities: Codeunit "SPBPL License Utilities"; + SPBPLenseUtilities: Codeunit "SPBPL License Utilities"; + begin + exit(Rec."Extension App Id" = SPBPLenseUtilities.GetTestProductAppId()); + end; + + internal procedure LaunchProductUrl() + var + SBPLicEvents: Codeunit "SPBPL Events"; + IsHandled: Boolean; begin - exit(Rec."Entry Id" = SPBPLLicenseUtilities.GetTestProductAppId()); + SBPLicEvents.OnBeforeLaunchProductUrl(Rec, IsHandled); + if not IsHandled then + Hyperlink("Product URL"); end; } diff --git a/src/codeunit/SPBLICIsoStoreManager.Codeunit.al b/src/Storage/SPBPLIsoStoreManager.Codeunit.al similarity index 53% rename from src/codeunit/SPBLICIsoStoreManager.Codeunit.al rename to src/Storage/SPBPLIsoStoreManager.Codeunit.al index 0aadb88..648f13d 100644 --- a/src/codeunit/SPBLICIsoStoreManager.Codeunit.al +++ b/src/Storage/SPBPLIsoStoreManager.Codeunit.al @@ -2,10 +2,31 @@ codeunit 71038 "SPBPL IsoStore Manager" { // Utility Codeunit var - EnvironmentInformation: Codeunit "Environment Information"; CryptographyManagement: Codeunit "Cryptography Management"; + EnvironmentInformation: Codeunit "Environment Information"; NameMapTok: Label '%1-%2', Comment = '%1 %2', Locked = true; + internal procedure UpdateOrCreateIsoStorage(var SPBExtensionLicense: Record "SPBPL Extension License") + var + SPBIsoStoreManager: Codeunit "SPBPL IsoStore Manager"; + YesterdayDateTime: DateTime; + begin + SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'lastUpdated', Format(CurrentDateTime, 0, 9)); + SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'endDate', Format(SPBExtensionLicense."Subscription End Date", 0, 9)); + SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'active', Format(SPBExtensionLicense.Activated, 0, 9)); + SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'preactivationDays', Format(SPBExtensionLicense."Sandbox Grace Days", 0, 9)); + + // We mark the lastCheckDate as yesterday on activation to trigger one check DURING activation, just to be safe + // Someone could be installing from an app file that's outdated, etc. + YesterdayDateTime := CreateDateTime(CalcDate('<-1D>', Today()), Time()); + SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'lastCheckDate', Format(YesterdayDateTime, 0, 9)); + + if not SPBIsoStoreManager.ContainsAppValue(SPBExtensionLicense, 'extensionContactEmail') then begin + SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'extensionContactEmail', SPBExtensionLicense."Billing Support Email"); + SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'extensionMisuseURI', ''); + end; + end; + internal procedure SetAppValue(SPBExtensionLicense: Record "SPBPL Extension License"; StoreName: Text; StoreValue: Text) begin if not IsolatedStorage.Contains(SPBExtensionLicense."Entry Id") then @@ -13,7 +34,7 @@ codeunit 71038 "SPBPL IsoStore Manager" if EnvironmentInformation.IsOnPrem() then if CryptographyManagement.IsEncryptionEnabled() and CryptographyManagement.IsEncryptionPossible() then - StoreValue := CryptographyManagement.Encrypt(StoreValue) + StoreValue := CryptographyManagement.EncryptText(CopyStr(StoreValue, 1, 215)) else if GuiAllowed() then Error('To use Spare Brained Licensing On-Prem, Database Encryption must be enabled.'); diff --git a/src/Telemetry/SPBPLTelemetry.Codeunit.al b/src/Telemetry/SPBPLTelemetry.Codeunit.al new file mode 100644 index 0000000..4fb9ce5 --- /dev/null +++ b/src/Telemetry/SPBPLTelemetry.Codeunit.al @@ -0,0 +1,117 @@ +codeunit 71048 "SPBPL Telemetry" +{ + Access = Internal; + + // Events to send: + // Self-Install / Update FYI + // New Extension Registered + // License activations / deactivation / platformDeactivation + // License check: Success/Fail + // Misuse report + // Version update check + + var + EventTagInstallMsg: Label 'Installation', Locked = true; + EventTagInstallTok: Label 'SPBPL1000', Locked = true; + EventTagUpgradeMsg: Label 'Upgraded', Locked = true; + EventTagUpgradeTok: Label 'SPBPL1001', Locked = true; + EventTagNewExtensionMsg: Label 'New Extension License Registered', Locked = true; + EventTagNewExtensionTok: Label 'SPBPL1002', Locked = true; + EventTagLicenseActivationMsg: Label 'License Activated', Locked = true; + EventTagLicenseActivationTok: Label 'SPBPL1100', Locked = true; + EventTagLicenseDeactivationMsg: Label 'License Deactivated', Locked = true; + EventTagLicenseDeactivationTok: Label 'SPBPL1101', Locked = true; + EventTagLicensePlatformDeactivationMsg: Label 'License Deactivated by Platform', Locked = true; + EventTagLicensePlatformDeactivationTok: Label 'SPBPL1102', Locked = true; + EventTagLicenseActivationFailureMsg: Label 'License Activation Failure', Locked = true; + EventTagLicenseActivationFailureTok: Label 'SPBPL1100', Locked = true; + EventTagLicenseCheckSuccessMsg: Label 'License Check - Success', Locked = true; + EventTagLicenseCheckSuccessTok: Label 'SPBPL1200', Locked = true; + EventTagLicenseCheckFailureMsg: Label 'License Check - Failed', Locked = true; + EventTagLicenseCheckFailureTok: Label 'SPBPL1201', Locked = true; + EventTagMisuseReportMsg: Label 'Misuse! Table data and IsoStorage mismatch', Locked = true; + EventTagMisuseReportTok: Label 'SPBPL1300', Locked = true; + EventTagVersionUpdateCheckMsg: Label 'Version Update Checking', Locked = true; + EventTagVersionUpdateCheckTok: Label 'SPBPL1400', Locked = true; + + internal procedure LicensingAppInstalled() + begin + EmitTraceTag(EventTagInstallMsg, EventTagInstallTok); + end; + + internal procedure LicensingAppUpgraded() + begin + EmitTraceTag(EventTagUpgradeMsg, EventTagUpgradeTok); + end; + + internal procedure NewExtensionRegistered(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + EmitTraceTag(SPBExtensionLicense, EventTagNewExtensionMsg, EventTagNewExtensionTok); + end; + + internal procedure LicenseActivation(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + EmitTraceTag(SPBExtensionLicense, EventTagLicenseActivationMsg, EventTagLicenseActivationTok); + end; + + internal procedure LicenseDeactivation(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + EmitTraceTag(SPBExtensionLicense, EventTagLicenseDeactivationMsg, EventTagLicenseDeactivationTok); + end; + + internal procedure LicensePlatformDeactivation(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + EmitTraceTag(SPBExtensionLicense, EventTagLicensePlatformDeactivationMsg, EventTagLicensePlatformDeactivationTok); + end; + + internal procedure LicenseActivationFailure(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + EmitTraceTag(SPBExtensionLicense, EventTagLicenseActivationFailureMsg, EventTagLicenseActivationFailureTok); + end; + + internal procedure LicenseCheckSuccess(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + EmitTraceTag(SPBExtensionLicense, EventTagLicenseCheckSuccessMsg, EventTagLicenseCheckSuccessTok); + end; + + internal procedure LicenseCheckFailure(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + EmitTraceTag(SPBExtensionLicense, EventTagLicenseCheckFailureMsg, EventTagLicenseCheckFailureTok); + end; + + internal procedure EventTagMisuseReport(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + EmitTraceTag(SPBExtensionLicense, EventTagMisuseReportMsg, EventTagMisuseReportTok); + end; + + internal procedure VersionUpdateCheck(var SPBExtensionLicense: Record "SPBPL Extension License") + begin + EmitTraceTag(SPBExtensionLicense, EventTagVersionUpdateCheckMsg, EventTagVersionUpdateCheckTok); + end; + + local procedure EmitTraceTag(EventDescriptionText: Text; Tag: Text) + var + TelemetryDimension: Dictionary of [Text, Text]; + begin + Session.LogMessage(Tag, EventDescriptionText, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, TelemetryDimension); + end; + + local procedure EmitTraceTag(var SPBExtensionLicense: Record "SPBPL Extension License"; EventDescriptionText: Text; Tag: Text) + var + TraceTagMessage: Text; + TelemetryDimension: Dictionary of [Text, Text]; + ExtensionNameLbl: Label 'ExtensionName', Locked = true; + ExtensionGuidLbl: Label 'ExtensionGuid', Locked = true; + ExtensionSubmoduleLbl: Label 'SubmoduleName', Locked = true; + ExtensionPublisherLbl: Label 'Publisher', Locked = true; + AppInfo: ModuleInfo; + begin + NavApp.GetModuleInfo(SPBExtensionLicense."Extension App Id", AppInfo); + TraceTagMessage := EventDescriptionText; + TelemetryDimension.Add(ExtensionNameLbl, SPBExtensionLicense."Extension Name"); + TelemetryDimension.Add(ExtensionGuidLbl, SPBExtensionLicense."Extension App Id"); + TelemetryDimension.Add(ExtensionPublisherLbl, AppInfo.Publisher); + TelemetryDimension.Add(ExtensionSubmoduleLbl, CompanyName); + Session.LogMessage(Tag, TraceTagMessage, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, TelemetryDimension); + end; +} diff --git a/src/page/SPBLICExtensionLicenses.Page.al b/src/UserInterface/SPBPLExtensionLicenses.Page.al similarity index 72% rename from src/page/SPBLICExtensionLicenses.Page.al rename to src/UserInterface/SPBPLExtensionLicenses.Page.al index 3840301..f79adf5 100644 --- a/src/page/SPBLICExtensionLicenses.Page.al +++ b/src/UserInterface/SPBPLExtensionLicenses.Page.al @@ -3,52 +3,46 @@ page 71033 "SPBPL Extension Licenses" ApplicationArea = All; Caption = 'Extension Licenses'; + Editable = false; PageType = List; SourceTable = "SPBPL Extension License"; UsageCategory = Administration; - Editable = false; layout { - area(content) + area(Content) { repeater(General) { field("Entry Id"; Rec."Entry Id") { ToolTip = 'This Guid is the Subscription Entry Id.'; - ApplicationArea = All; Visible = false; } field("Extension App Id"; Rec."Extension App Id") { ToolTip = 'This Guid is the Extension''s App Id.'; - ApplicationArea = All; Visible = false; } field("Extension Name"; Rec."Extension Name") { - ToolTip = 'The name of the Extension that is registered to have a Subscription requirement.'; - ApplicationArea = All; StyleExpr = SubscriptionStatusStyle; + ToolTip = 'The name of the Extension that is registered to have a Subscription requirement.'; } field("Submodule Name"; Rec."Submodule Name") { ToolTip = 'If this Extension uses Module based Subscriptions, this displays which Submodule/Edition this is.'; - ApplicationArea = All; } field(Activated; Rec.Activated) { ToolTip = 'Shows if this Extension has been Activated with a Product Key.'; - ApplicationArea = All; } field(UpdateLink; UpdateLink) { Caption = 'Update News'; - ToolTip = 'If an Update is available, this will link to where to find out more.'; - ApplicationArea = All; DrillDown = true; Style = Favorable; + ToolTip = 'If an Update is available, this will link to where to find out more.'; trigger OnDrillDown() begin @@ -58,31 +52,31 @@ page 71033 "SPBPL Extension Licenses" field("Trial Grace End Date"; Rec."Trial Grace End Date") { ToolTip = 'If the Extension is not yet Activated, this is the last date the Extension can run in Trial Mode.'; - ApplicationArea = All; } field("Subscription Email"; Rec."Subscription Email") { - ToolTip = 'This shows the email address that the License Key is registered to, in case there is a need to find it later.'; ExtendedDatatype = EMail; - ApplicationArea = All; + ToolTip = 'This shows the email address that the License Key is registered to, in case there is a need to find it later.'; } field("Product URL"; Rec."Product URL") { - ToolTip = 'The page where one can find more information about purchasing a Subscription for this Extension.'; - ApplicationArea = All; ExtendedDatatype = URL; + ToolTip = 'The page where one can find more information about purchasing a Subscription for this Extension.'; } field("Support URL"; Rec."Support URL") { - ToolTip = 'The page where one can find more information about how to get Support for the Extension.'; - ApplicationArea = All; ExtendedDatatype = URL; + ToolTip = 'The page where one can find more information about how to get Support for the Extension.'; } field("Billing Support Email"; Rec."Billing Support Email") { - ToolTip = 'The email address to contact with Billing related questions about this Subscription.'; - ApplicationArea = All; ExtendedDatatype = EMail; + ToolTip = 'The email address to contact with Billing related questions about this Subscription.'; + } + field("License Platform"; Rec."License Platform") + { + ToolTip = 'Specifies the value of the License Platform field.'; + Visible = false; } } } @@ -94,26 +88,22 @@ page 71033 "SPBPL Extension Licenses" { action(ActivateProduct) { - ApplicationArea = All; Caption = 'Activate'; Enabled = not Rec.Activated and UserHasWritePermission; Image = SuggestElectronicDocument; Promoted = true; - PromotedOnly = true; PromotedCategory = Process; + PromotedOnly = true; ToolTip = 'Launches the Activation Wizard for this Subscription.'; trigger OnAction() - var - SPBLicenseManagement: Codeunit "SPBPL License Management"; begin - SPBLicenseManagement.LaunchActivation(Rec); + LaunchActivation(Rec); Rec.SetRange("Entry Id"); end; } action(DeactivateProduct) { - ApplicationArea = All; Caption = 'Deactivate'; Enabled = Rec.Activated and UserHasWritePermission; Image = Cancel; @@ -123,10 +113,8 @@ page 71033 "SPBPL Extension Licenses" ToolTip = 'Forces this Subscription inactive, which will allow entry of a new License Key.'; trigger OnAction() - var - SPBLicenseManagement: Codeunit "SPBPL License Management"; begin - SPBLicenseManagement.DeactivateExtension(Rec); + DeactivateExtension(Rec); Rec.SetRange("Entry Id"); end; } @@ -136,10 +124,9 @@ page 71033 "SPBPL Extension Licenses" var UserHasWritePermission: Boolean; - ShowAsTestSubscription: Boolean; + UpdateAvailableTok: Label 'Available'; SubscriptionStatusStyle: Text; UpdateLink: Text; - UpdateAvailableTok: Label 'Available'; trigger OnOpenPage() begin @@ -152,7 +139,6 @@ page 71033 "SPBPL Extension Licenses" trigger OnAfterGetRecord() begin - ShowAsTestSubscription := Rec.IsTestSubscription(); SetSubscriptionStyle(); if Rec."Update Available" and (Rec."Update News URL" <> '') then @@ -182,13 +168,43 @@ page 71033 "SPBPL Extension Licenses" local procedure CheckAllForUpdates() var - SPBExtensionLicense: Record "SPBPL Extension License"; - SPBPLLicenseManagement: Codeunit "SPBPL License Management"; + SPBPLense: Record "SPBPL Extension License"; + SPBPLVersionCheck: Codeunit "SPBPL Version Check"; begin - if SPBExtensionLicense.FindSet(true) then + if SPBPLense.FindSet(true) then repeat - if SPBExtensionLicense."Update News URL" <> '' then - SPBPLLicenseManagement.DoVersionCheck(SPBExtensionLicense); - until SPBExtensionLicense.Next() = 0; + if SPBPLense."Update News URL" <> '' then + SPBPLVersionCheck.DoVersionCheck(SPBPLense); + until SPBPLense.Next() = 0; + end; + + internal procedure LaunchActivation(var SPBExtensionLicense: Record "SPBPL Extension License") + var + SPBPLenseActivationWizard: Page "SPBPL License Activation"; + begin + Clear(SPBPLenseActivationWizard); + SPBExtensionLicense.SetRecFilter(); + SPBPLenseActivationWizard.SetTableView(SPBExtensionLicense); + SPBPLenseActivationWizard.RunModal(); + end; + + internal procedure DeactivateExtension(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean + var + SPBPLDeactivateMeth: Codeunit "SPBPL Deactivate Meth"; + DoDeactivation: Boolean; + LicensePlatform: Interface "SPBPL ILicenseCommunicator"; + DeactivationNotPossibleWarningQst: Label 'This will deactivate this license in this Business Central instance, but you will need to contact the Publisher to release the assigned license. \ \Are you sure you want to deactivate this license?'; + DeactivationPossibleQst: Label 'This will deactivate this license in this Business Central instance.\ \Are you sure you want to deactivate this license?'; + begin + LicensePlatform := SPBExtensionLicense."License Platform"; + + // Depending on the platform capabilities, we give the user a different message + if LicensePlatform.ClientSideDeactivationPossible(SPBExtensionLicense) then + DoDeactivation := Confirm(DeactivationPossibleQst, false) + else + DoDeactivation := Confirm(DeactivationNotPossibleWarningQst, false); + + if DoDeactivation then + exit(SPBPLDeactivateMeth.Deactivate(SPBExtensionLicense, false)); end; } \ No newline at end of file diff --git a/src/page/SPBLICLicenseActivation.Page.al b/src/UserInterface/SPBPLLicenseActivation.Page.al similarity index 90% rename from src/page/SPBLICLicenseActivation.Page.al rename to src/UserInterface/SPBPLLicenseActivation.Page.al index 626d305..5b81fe9 100644 --- a/src/page/SPBLICLicenseActivation.Page.al +++ b/src/UserInterface/SPBPLLicenseActivation.Page.al @@ -1,13 +1,14 @@ page 71034 "SPBPL License Activation" { + ApplicationArea = All; Caption = 'Licensing Activation Wizard'; PageType = NavigatePage; SourceTable = "SPBPL Extension License"; layout { - area(content) + area(Content) { group(Step1) { @@ -17,19 +18,18 @@ page 71034 "SPBPL License Activation" Caption = 'This is the Activation Wizard for the Licensing system.'; group(WelcomeExplainer) { - ShowCaption = false; InstructionalText = 'You will enter the License Key for the following License. If you do not have a License key, please click on the "Get License Key" link.'; + ShowCaption = false; } field(LicenseLinkField; LicenseLinkText) { DrillDown = true; Editable = false; ShowCaption = false; - ApplicationArea = All; trigger OnDrillDown() begin - SPBPLLicenseUtilities.LaunchProductUrl(Rec); + Rec.LaunchProductUrl(); end; } } @@ -38,8 +38,8 @@ page 71034 "SPBPL License Activation" Caption = 'Start Activation'; group(StartText) { - ShowCaption = false; InstructionalText = 'Click Next to begin the activation process. You will need your Subscription License key.'; + ShowCaption = false; } } } @@ -49,20 +49,18 @@ page 71034 "SPBPL License Activation" group(LicenseKeyInstruction) { - ShowCaption = false; InstructionalText = 'Please enter your Subscription License Key:'; + ShowCaption = false; } field(LicenseKeyField; LicenseKey) { - ApplicationArea = All; - ShowMandatory = true; ShowCaption = false; + ShowMandatory = true; } field(LicenseKeyFormat; LicenseFormatHintText) { - ApplicationArea = All; - ShowCaption = false; Editable = false; + ShowCaption = false; } } group(Step2Test) @@ -71,36 +69,33 @@ page 71034 "SPBPL License Activation" group(TestPathLicenseKeyInstruction) { - ShowCaption = false; InstructionalText = 'Please enter your Subscription License Key:'; + ShowCaption = false; } field(TestPathLicenseKeyField; LicenseKey) { - ApplicationArea = All; - ShowMandatory = true; ShowCaption = false; + ShowMandatory = true; } field(TestPathLicenseKeyFormat; LicenseFormatHintText) { - ApplicationArea = All; - ShowCaption = false; Editable = false; + ShowCaption = false; } group(TestLicenseOption) { - ShowCaption = false; InstructionalText = 'This is the Test Subscription, so you can use a test License Key here by clicking:'; + ShowCaption = false; } field(TestLicenseHint; TestLicenseLinkText) { DrillDown = true; Editable = false; ShowCaption = false; - ApplicationArea = All; trigger OnDrillDown() begin - LicenseKey := SPBPLLicenseUtilities.GetTestProductKey(); + LicenseKey := SPBPLenseUtilities.GetTestProductKey(Rec); end; } } @@ -112,15 +107,15 @@ page 71034 "SPBPL License Activation" Caption = 'Activation Results'; group(ActivationWorked) { - Visible = ActivationResult; InstructionalText = 'Activation Successful!'; ShowCaption = false; + Visible = ActivationResult; } group(ActivationDidNotWork) { - Visible = not ActivationResult; InstructionalText = 'Activation Unsuccessful!'; ShowCaption = false; + Visible = not ActivationResult; } } } @@ -129,11 +124,10 @@ page 71034 "SPBPL License Activation" actions { - area(processing) + area(Processing) { action(ActionBack) { - ApplicationArea = All; Caption = 'Back'; Enabled = BackActionEnabled; Image = PreviousRecord; @@ -146,7 +140,6 @@ page 71034 "SPBPL License Activation" } action(ActionNext) { - ApplicationArea = All; Caption = 'Next'; Enabled = NextActionEnabled; Image = NextRecord; @@ -161,7 +154,6 @@ page 71034 "SPBPL License Activation" } action(ActionFinish) { - ApplicationArea = All; Caption = 'Finish'; Enabled = FinishActionEnabled; Image = Approve; @@ -176,25 +168,25 @@ page 71034 "SPBPL License Activation" } var - SPBPLLicenseManagement: Codeunit "SPBPL License Management"; - SPBPLLicenseUtilities: Codeunit "SPBPL License Utilities"; - LicenseLinkText: Text; - LicenseKey: Text; - LicenseFormatHintText: Text; - TestLicenseLinkText: Text; - Step1Visible: Boolean; - Step2Visible: Boolean; - Step2TestVisible: Boolean; - Step3Visible: Boolean; - Step: Option Start,Step2,Step2Test,Finish; + SPBPLActivateMeth: Codeunit "SPBPL Activate Meth"; + SPBPLenseUtilities: Codeunit "SPBPL License Utilities"; + ActivationResult: Boolean; BackActionEnabled: Boolean; FinishActionEnabled: Boolean; NextActionEnabled: Boolean; - LicenseLinkUriTok: Label 'Get License Key'; + ShowAsTestSubscription: Boolean; + Step1Visible: Boolean; + Step2TestVisible: Boolean; + Step2Visible: Boolean; + Step3Visible: Boolean; LicenseKeyNeededErr: Label 'You need to enter a License Key to continue.'; + LicenseLinkUriTok: Label 'Get License Key'; TestLicenseKeyOfferTok: Label 'Use Test Key'; - ActivationResult: Boolean; - ShowAsTestSubscription: Boolean; + Step: Option Start,Step2,Step2Test,Finish; + LicenseFormatHintText: Text; + LicenseKey: Text; + LicenseLinkText: Text; + TestLicenseLinkText: Text; trigger OnOpenPage() @@ -238,7 +230,8 @@ page 71034 "SPBPL License Activation" // Validation trigger when moving from Step2 to 3 if (Step = Step::Step2) and not Backwards then begin Rec."License Key" := CopyStr(LicenseKey, 1, MaxStrLen(Rec."License Key")); - ActivationResult := SPBPLLicenseManagement.ActivateFromWizard(Rec); + Rec.Modify(); + ActivationResult := SPBPLActivateMeth.Activate(Rec); Step := Step + 1; end; @@ -247,7 +240,7 @@ page 71034 "SPBPL License Activation" if Backwards then Step := Step - 1 - ELSE + else Step := Step + 1; EnableControls(); diff --git a/src/codeunit/SPBLICLicenseManagement.Codeunit.al b/src/codeunit/SPBLICLicenseManagement.Codeunit.al deleted file mode 100644 index 573edab..0000000 --- a/src/codeunit/SPBLICLicenseManagement.Codeunit.al +++ /dev/null @@ -1,274 +0,0 @@ -codeunit 71036 "SPBPL License Management" -{ - Permissions = tabledata "SPBPL Extension License" = RIM; - - var - SPBLicenseUtilities: Codeunit "SPBPL License Utilities"; - - internal procedure CheckSupportedVersion(minVersion: Version) - var - VersionUpdateRequiredErr: Label 'To install this Extension, you need to update %1 to at least version %2.', Comment = '%1 is the name of extension, %2 is the version number'; - AppInfo: ModuleInfo; - begin - NavApp.GetCurrentModuleInfo(AppInfo); - if AppInfo.AppVersion < minVersion then - if GuiAllowed then - Error(VersionUpdateRequiredErr, AppInfo.Name, minVersion); - end; - - - internal procedure ActivateFromWizard(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean - var - ResponseBody: Text; - NoRemainingUsesErr: Label 'There are no remaining uses of that license key to assign to this installation.'; - LicensePlatform: Interface "SPBPL ILicenseCommunicator"; - begin - LicensePlatform := SPBExtensionLicense."License Platform"; - // We call first WITHOUT incrementing the use count to check current use count - if LicensePlatform.CallAPIForVerification(SPBExtensionLicense, ResponseBody, false) then - if LicensePlatform.CheckAPILicenseCount(SPBExtensionLicense, ResponseBody) then - exit(ActivateExtension(SPBExtensionLicense, ResponseBody)) - else - if GuiAllowed() then - Error(NoRemainingUsesErr); - end; - - internal procedure LaunchActivation(var SPBExtensionLicense: Record "SPBPL Extension License") - var - SPBLicenseActivationWizard: Page "SPBPL License Activation"; - begin - Clear(SPBLicenseActivationWizard); - SPBExtensionLicense.SetRecFilter(); - SPBLicenseActivationWizard.SetTableView(SPBExtensionLicense); - SPBLicenseActivationWizard.RunModal(); - end; - - internal procedure VerifyActiveLicense(var SPBExtensionLicense: Record "SPBPL Extension License") - begin - if not SPBExtensionLicense.Activated then - exit; - - if not CheckIfActive(SPBExtensionLicense) then begin - SPBExtensionLicense.Validate(Activated, false); - SPBExtensionLicense.Modify(); - end; - end; - - internal procedure CheckIfActive(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean - var - SPBIsoStoreManager: Codeunit "SPBPL IsoStore Manager"; - EnvironmentInformation: Codeunit "Environment Information"; - GraceExpiringMsg: Label 'Today is the last trial day for %1. Please purchase a License Key and Activate the subscription to continue use.', Comment = '%1 is the name of the Extension'; - DaysGraceTok: Label '<+%1D>', Comment = '%1 is the number of days'; - IsoActive: Boolean; - ResponseBody: Text; - GraceEndDate: Date; - InstallDateTime: DateTime; - LastCheckDateTime: DateTime; - IsoDatetime: DateTime; - IsoNumber: Integer; - LicensePlatform: Interface "SPBPL ILicenseCommunicator"; - begin - LicensePlatform := SPBExtensionLicense."License Platform"; - - // if the subscription isn't active, check if we're in the 'grace' preinstall window, which always includes the first day of use - if not SPBExtensionLicense.Activated then begin - Evaluate(InstallDateTime, SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'installDate')); - Evaluate(IsoNumber, SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'preactivationDays')); - if IsoNumber > 0 then - GraceEndDate := CalcDate(StrSubstNo(DaysGraceTok, IsoNumber), DT2Date(InstallDateTime)) - else - if (EnvironmentInformation.IsSandbox() and (IsoNumber < 0)) then - // -1 days grace for a Sandbox means it's unlimited use in sandboxes, even if not activated. - exit(true) - else - GraceEndDate := Today; - if (GraceEndDate = Today) and GuiAllowed then - Message(GraceExpiringMsg, SPBExtensionLicense."Extension Name"); - exit(GraceEndDate > Today); - end; - - Evaluate(LastCheckDateTime, SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'lastCheckDate')); - if ((Today() - DT2Date(LastCheckDateTime)) > 0) then begin - if LicensePlatform.CallAPIForVerification(SPBExtensionLicense, ResponseBody, false) then begin - // This may update the End Dates - note: may or may not call .Modify - LicensePlatform.PopulateSubscriptionFromResponse(SPBExtensionLicense, ResponseBody); - SPBExtensionLicense.Modify(); - end; - DoVersionCheck(SPBExtensionLicense); - SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'lastCheckDate', Format(CurrentDateTime, 0, 9)); - end; - - // if the subscription ran out - if (SPBExtensionLicense."Subscription End Date" < CurrentDateTime) and - (SPBExtensionLicense."Subscription End Date" <> 0DT) - then - exit(false); - - - // if the record version IS active, then let's crosscheck against isolated storage - Evaluate(IsoActive, SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'active')); - if not IsoActive then - LicensePlatform.ReportPossibleMisuse(SPBExtensionLicense); - - // Check Record end date against IsoStorage end date - Evaluate(IsoDatetime, SPBIsoStoreManager.GetAppValue(SPBExtensionLicense, 'endDate')); - if IsoDatetime <> 0DT then - // Only checking at the date level in case of time zone nonsense - if DT2Date(IsoDatetime) <> DT2Date(SPBExtensionLicense."Subscription End Date") then - LicensePlatform.ReportPossibleMisuse(SPBExtensionLicense); - - // Finally, all things checked out - exit(true); - end; - - local procedure ActivateExtension(var SPBExtensionLicense: Record "SPBPL Extension License"; ResponseBody: Text): Boolean - var - LicensePlatform: Interface "SPBPL ILicenseCommunicator"; - AppInfo: ModuleInfo; - ActivationFailureErr: Label 'An error occured validating the license. Contact %1 for assistance', Comment = '%1 is the App Publisher'; - LicenseKeyExpiredErr: Label 'The License Key provided has already expired due to a Subscription End. Contact %1 for assistance', Comment = '%1 is the App Publisher'; - begin - NavApp.GetModuleInfo(SPBExtensionLicense."Extension App Id", AppInfo); - - // Note we're swapping the ResponseBody to the 2nd API call with the new info from the API! - LicensePlatform := SPBExtensionLicense."License Platform"; - if LicensePlatform.CallAPIForVerification(SPBExtensionLicense, ResponseBody, true) then begin - - LicensePlatform.PopulateSubscriptionFromResponse(SPBExtensionLicense, ResponseBody); - - if (SPBExtensionLicense."Subscription End Date" <> 0DT) and - (SPBExtensionLicense."Subscription End Date" < CurrentDateTime) - then begin - SPBExtensionLicense.Activated := false; - SPBExtensionLicense.Modify(); - Commit(); - OnAfterActivationFailure(SPBExtensionLicense, AppInfo); - if GuiAllowed() then - Error(LicenseKeyExpiredErr, AppInfo.Publisher); - end else begin - SPBExtensionLicense.Modify(); - Commit(); - OnAfterActivationSuccess(SPBExtensionLicense, AppInfo); - end; - - // Now pop the details into IsolatedStorage - SPBLicenseUtilities.UpdateOrCreateIsoStorage(SPBExtensionLicense); - exit(SPBExtensionLicense.Activated); - end; - end; - - internal procedure DoVersionCheck(var SPBExtensionLicense: Record "SPBPL Extension License") - var - UserTask: Record "User Task"; - SubjectTok: Label 'Update Extension: %1', Comment = '%1 is Extension Name'; - DocsTok: Label 'https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/tenant-admin-center-manage-apps#get-an-overview-and-check-for-updates', Locked = true; - ApiHttpClient: HttpClient; - ApiHttpRequestMessage: HttpRequestMessage; - ApiHttpResponseMessage: HttpResponseMessage; - VersionResponseBody: Text; - IsHandled: Boolean; - LatestVersion: Version; - AppInfo: ModuleInfo; - begin - NavApp.GetCurrentModuleInfo(AppInfo); - if SPBExtensionLicense."Version Check URL" = '' then - exit; - - ApiHttpRequestMessage.SetRequestUri(SPBExtensionLicense."Version Check URL"); - ApiHttpRequestMessage.Method('GET'); - - if ApiHttpClient.Send(ApiHttpRequestMessage, ApiHttpResponseMessage) then begin - if ApiHttpResponseMessage.IsSuccessStatusCode then begin - ApiHttpResponseMessage.Content.ReadAs(VersionResponseBody); - LatestVersion := Version.Create(VersionResponseBody); - if (AppInfo.AppVersion < LatestVersion) then begin - SPBExtensionLicense."Update Available" := true; - SPBExtensionLicense.Modify(); - - OnBeforeVersionCheckUpgradeAvailable(SPBExtensionLicense, LatestVersion, IsHandled); - if IsHandled then - exit; - - UserTask.Init(); - UserTask.Title := StrSubstNo(SubjectTok, AppInfo.Name); - UserTask.SetDescription(DocsTok); - if not IsNullGuid(SPBExtensionLicense."Activated By") then - UserTask."Assigned To" := SPBExtensionLicense."Activated By"; - UserTask."Due DateTime" := CurrentDateTime; - UserTask."Start DateTime" := CurrentDateTime; - UserTask."Object Type" := UserTask."Object Type"::Page; - UserTask."Object ID" := Page::"SPBPL Extension Licenses"; - UserTask.Insert(true); - end; - end else - OnAfterVersionCheckFailure(SPBExtensionLicense, ApiHttpResponseMessage); - end else - OnAfterVersionCheckFailure(SPBExtensionLicense, ApiHttpResponseMessage); - end; - - internal procedure DeactivateExtension(var SPBExtensionLicense: Record "SPBPL Extension License"): Boolean - var - DeactivationWarningQst: Label 'This will deactivate this license in this Business Central instance, but you will need to contact the Publisher to release the assigned license. \ \Are you sure you want to deactivate this license?'; - begin - if Confirm(DeactivationWarningQst, false) then begin - DeactivateLicense(SPBExtensionLicense) - end; - end; - - - internal procedure DeactivateLicense(var SPBExtensionLicense: Record "SPBPL Extension License") - var - SPBLicenseUtilities: Codeunit "SPBPL License Utilities"; - begin - SPBExtensionLicense.Validate(Activated, false); - SPBExtensionLicense.Modify(); - SPBLicenseUtilities.UpdateOrCreateIsoStorage(SPBExtensionLicense); - OnAfterLicenseDeactivated(SPBExtensionLicense); - end; - - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Environment Triggers", 'OnAfterCopyEnvironmentPerDatabase', '', false, false)] - local procedure DeactivateLicensesWhenEnvironmentCopied(DestinationEnvironmentType: Option Production,Sandbox) - var - SPBExtensionLicense: Record "SPBPL Extension License"; - begin - if SPBExtensionLicense.FindSet(true) then - repeat - if DestinationEnvironmentType = DestinationEnvironmentType::Sandbox then begin - // Reset all licenses to Sandbox grace - if SPBExtensionLicense."Sandbox Grace Days" <> 0 then - SPBExtensionLicense."Trial Grace End Date" := CalcDate(StrSubstNo('<+%1D>', SPBExtensionLicense."Sandbox Grace Days"), Today); - DeactivateLicense(SPBExtensionLicense); - end else begin - // Deactive the licenses in general - DeactivateLicense(SPBExtensionLicense); - end; - until SPBExtensionLicense.Next() = 0; - end; - - [IntegrationEvent(false, false)] - local procedure OnAfterLicenseDeactivated(var SPBExtensionLicense: Record "SPBPL Extension License") - begin - end; - - [IntegrationEvent(false, false)] - local procedure OnAfterActivationFailure(var SPBExtensionLicense: Record "SPBPL Extension License"; var AppInfo: ModuleInfo) - begin - end; - - [IntegrationEvent(false, false)] - local procedure OnBeforeVersionCheckUpgradeAvailable(var SPBExtensionLicense: Record "SPBPL Extension License"; var LatestVersion: Version; var IsHandled: Boolean) - begin - end; - - [IntegrationEvent(false, false)] - local procedure OnAfterActivationSuccess(var SPBExtensionLicense: Record "SPBPL Extension License"; var AppInfo: ModuleInfo) - begin - end; - - [IntegrationEvent(false, false)] - local procedure OnAfterVersionCheckFailure(var SPBExtensionLicense: Record "SPBPL Extension License"; var ApiHttpResponseMessage: HttpResponseMessage) - begin - end; - -} diff --git a/src/codeunit/SPBLICLicenseUtilities.Codeunit.al b/src/codeunit/SPBLICLicenseUtilities.Codeunit.al deleted file mode 100644 index 3360a98..0000000 --- a/src/codeunit/SPBLICLicenseUtilities.Codeunit.al +++ /dev/null @@ -1,71 +0,0 @@ -codeunit 71033 "SPBPL License Utilities" -{ - - var - SelfTestProductIdTok: Label 'bwdCu', Locked = true; - SelfTestProductKeyTok: Label '21E2339D-F24D4A92-9813B4F2-8ABA083C', Locked = true; - - internal procedure GetTestProductAppId(): Guid - var - AppInfo: ModuleInfo; - begin - NavApp.GetCurrentModuleInfo(AppInfo); - exit(AppInfo.Id); - end; - - internal procedure GetTestProductId(): Text - begin - exit(SelfTestProductIdTok); - end; - - internal procedure GetTestProductKey(): Text - begin - exit(SelfTestProductKeyTok); - end; - - internal procedure LaunchProductUrl(SPBExtensionLicense: Record "SPBPL Extension License") - var - IsHandled: Boolean; - begin - OnBeforeLaunchProductUrl(SPBExtensionLicense, IsHandled); - if not IsHandled then - Hyperlink(SPBExtensionLicense."Product URL"); - end; - - internal procedure AddNameValuePair(var FormData: TextBuilder; name: Text; value: Text) - var - FormDataNameTok: Label 'Content-Disposition: form-data; name="%1"', Comment = '%1 is the name of the Form Data', Locked = true; - begin - FormData.AppendLine('--SpareBrainedLicensing'); - FormData.AppendLine(StrSubstNo(FormDataNameTok, name)); - FormData.AppendLine(); - FormData.AppendLine(value); - FormData.AppendLine('--SpareBrainedLicensing--'); - end; - - internal procedure UpdateOrCreateIsoStorage(var SPBExtensionLicense: Record "SPBPL Extension License") - var - SPBIsoStoreManager: Codeunit "SPBPL IsoStore Manager"; - YesterdayDateTime: DateTime; - begin - SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'lastUpdated', Format(CurrentDateTime, 0, 9)); - SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'endDate', Format(SPBExtensionLicense."Subscription End Date", 0, 9)); - SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'active', Format(SPBExtensionLicense.Activated, 0, 9)); - SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'preactivationDays', Format(SPBExtensionLicense."Sandbox Grace Days", 0, 9)); - - // We mark the lastCheckDate as yesterday on activation to trigger one check DURING activation, just to be safe - // Someone could be installing from an app file that's outdated, etc. - YesterdayDateTime := CreateDateTime(CalcDate('<-1D>', Today()), Time()); - SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'lastCheckDate', Format(YesterdayDateTime, 0, 9)); - - if not SPBIsoStoreManager.ContainsAppValue(SPBExtensionLicense, 'extensionContactEmail') then begin - SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'extensionContactEmail', SPBExtensionLicense."Billing Support Email"); - SPBIsoStoreManager.SetAppValue(SPBExtensionLicense, 'extensionMisuseURI', ''); - end; - end; - - [IntegrationEvent(false, false)] - local procedure OnBeforeLaunchProductUrl(var SPBExtensionLicense: Record "SPBPL Extension License"; var IsHandled: Boolean) - begin - end; -} diff --git a/src/codeunit/SPBLICLicensingInstall.Codeunit.al b/src/codeunit/SPBLICLicensingInstall.Codeunit.al deleted file mode 100644 index f4db801..0000000 --- a/src/codeunit/SPBLICLicensingInstall.Codeunit.al +++ /dev/null @@ -1,25 +0,0 @@ -codeunit 71037 "SPBPL Licensing Install" -{ - Subtype = Install; - - trigger OnInstallAppPerDatabase() - var - SPBExtensionLicense: Record "SPBPL Extension License"; - SPBPLLicenseUtilities: Codeunit "SPBPL License Utilities"; - AppInfo: ModuleInfo; - begin - NavApp.GetCurrentModuleInfo(AppInfo); - // We install / update the 'Test' entry - if not SPBExtensionLicense.Get(AppInfo.Id) then begin - SPBExtensionLicense.Init(); - Evaluate(SPBExtensionLicense."Entry Id", AppInfo.Id); - SPBExtensionLicense.Insert(true); - end; - SPBExtensionLicense."Extension Name" := AppInfo.Name + ' Test Subcription'; - SPBExtensionLicense."Product Code" := CopyStr(SPBPLLicenseUtilities.GetTestProductId(), 1, MaxStrLen(SPBExtensionLicense."Product Code")); - SPBExtensionLicense."Product URL" := 'https://sparebrained.gumroad.com/l/SBILicensingTest'; - SPBExtensionLicense."Support URL" := 'support@sparebrained.com'; - SPBExtensionLicense."Billing Support Email" := 'support@sparebrained.com'; - SPBExtensionLicense.Modify(); - end; -} diff --git a/src/interface/SPBLICILicenseCommunicator.Interface.al b/src/interface/SPBLICILicenseCommunicator.Interface.al deleted file mode 100644 index d309066..0000000 --- a/src/interface/SPBLICILicenseCommunicator.Interface.al +++ /dev/null @@ -1,12 +0,0 @@ -interface "SPBPL ILicenseCommunicator" -{ - procedure CallAPIForVerification(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text; IncrementLicenseCount: Boolean) ResultOK: Boolean; - - procedure ReportPossibleMisuse(SPBExtensionLicense: Record "SPBPL Extension License"); - - procedure PopulateSubscriptionFromResponse(var SPBExtensionLicense: Record "SPBPL Extension License"; var ResponseBody: Text); - - procedure CheckAPILicenseCount(var SPBExtensionLicense: Record "SPBPL Extension License"; ResponseBody: Text): Boolean - - procedure SampleKeyFormatText(): Text; -} diff --git a/src/permissionset/SPBLICLicensing.PermissionSet.al b/src/permissionset/SPBLICLicensing.PermissionSet.al deleted file mode 100644 index 7b60fb4..0000000 --- a/src/permissionset/SPBLICLicensing.PermissionSet.al +++ /dev/null @@ -1,14 +0,0 @@ -permissionset 71034 "SPBPL Licensing" -{ - Assignable = true; - Caption = 'Spare Brained Licensing Admin'; - Permissions = - table "SPBPL Extension License" = X, - tabledata "SPBPL Extension License" = RMI, - codeunit "SPBPL Gumroad Communicator" = X, - codeunit "SPBPL Licensing Install" = X, - codeunit "SPBPL License Management" = X, - codeunit "SPBPL License Utilities" = X, - page "SPBPL License Activation" = X, - page "SPBPL Extension Licenses" = X; -} diff --git a/src/permissionset/SPBLICLicensingRO.PermissionSet.al b/src/permissionset/SPBLICLicensingRO.PermissionSet.al deleted file mode 100644 index 5dc07d0..0000000 --- a/src/permissionset/SPBLICLicensingRO.PermissionSet.al +++ /dev/null @@ -1,16 +0,0 @@ -permissionset 71035 "SPBPL Licensing RO" -{ - Assignable = true; - Caption = 'SPBPL Licensing RO'; - Permissions = - table "SPBPL Extension License" = X, - tabledata "SPBPL Extension License" = RI, - codeunit "SPBPL License Management" = X, - codeunit "SPBPL Licensing Install" = X, - codeunit "SPBPL License Utilities" = X, - codeunit "SPBPL IsoStore Manager" = X, - codeunit "SPBPL Gumroad Communicator" = X, - codeunit "SPBPL Extension Registration" = X, - page "SPBPL License Activation" = X, - page "SPBPL Extension Licenses" = X; -} diff --git a/src/permissionsetextension/SPBLICIndirectRead.PermissionSetExt.al b/src/permissionsetextension/SPBLICIndirectRead.PermissionSetExt.al deleted file mode 100644 index 2c28ad5..0000000 --- a/src/permissionsetextension/SPBLICIndirectRead.PermissionSetExt.al +++ /dev/null @@ -1,4 +0,0 @@ -permissionsetextension 71033 "SPBPL Indirect Read" extends "D365 READ" -{ - Permissions = tabledata "SPBPL Extension License" = rim; -}