{"ArticleId":null,"Name":"Plugin provider types","Content":"\u003Cp\u003EA plugin becomes useful when it implements one of the provider interfaces the platform asks for at the right moment: checkout asks payment and shipping providers, the storefront asks widget providers what to render in each zone, and discounts ask discount rules whether a requirement is met. This article lists the four most common contracts with a minimal implementation of each. The plugin scaffolding around them is described in \u003Ca href=\u0022/developers-writing-a-plugin\u0022\u003EWriting a plugin\u003C/a\u003E.\u003C/p\u003E\n\n\u003Ch2 id=\u0022iprovider\u0022\u003EThe common base: IProvider\u003C/h2\u003E\n\u003Cp\u003EEvery provider interface extends \u003Ccode\u003EIProvider\u003C/code\u003E:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic interface IProvider\n{\n    string ConfigurationUrl { get; }\n    string SystemName { get; }\n    string FriendlyName { get; }\n    int Priority { get; }\n    IList\u0026lt;string\u0026gt; LimitedToStores { get; }\n    IList\u0026lt;string\u0026gt; LimitedToGroups { get; }\n}\u003C/code\u003E\u003C/pre\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003EMember\u003C/th\u003E\u003Cth\u003EWhat it does\u003C/th\u003E\u003C/tr\u003E\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003ESystemName\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EMust equal the manifest system name; used to store which providers are active.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EFriendlyName\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EThe name shown to operators and customers. Resolve it from a resource key with \u003Ccode\u003EITranslationService\u003C/code\u003E.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EPriority\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003ESort order, usually read from a \u003Ccode\u003EDisplayOrder\u003C/code\u003E setting so operators control it.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003ELimitedToStores\u003C/code\u003E, \u003Ccode\u003ELimitedToGroups\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EStore ids and customer group ids the provider is limited to. An empty list means available everywhere.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EConfigurationUrl\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EThe admin route of the configuration screen, or empty.\u003C/td\u003E\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\n\u003Cp\u003ERegister each provider as scoped in the plugin\u0027s \u003Ccode\u003EStartupApplication\u003C/code\u003E: \u003Ccode\u003Eservices.AddScoped\u0026lt;IWidgetProvider, MyWidgetProvider\u0026gt;();\u003C/code\u003E\u003C/p\u003E\n\n\u003Ch2 id=\u0022payment\u0022\u003EPayment methods: IPaymentProvider\u003C/h2\u003E\n\u003Cp\u003EA payment provider takes part in checkout and in order management. The platform calls it to decide whether the method is shown, to calculate an additional fee, to process the payment when the order is placed and, for redirection gateways, to send the customer to the payment page afterwards. Capture, refund and void are called from the order screen when the provider supports them.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic class ExamplePaymentProvider(\n    ITranslationService translationService,\n    ExamplePaymentSettings settings) : IPaymentProvider\n{\n    public string ConfigurationUrl =\u0026gt; ExamplePaymentDefaults.ConfigurationUrl;\n    public string SystemName =\u0026gt; ExamplePaymentDefaults.ProviderSystemName;\n    public string FriendlyName =\u0026gt; translationService.GetResource(ExamplePaymentDefaults.FriendlyName);\n    public int Priority =\u0026gt; settings.DisplayOrder;\n    public IList\u0026lt;string\u0026gt; LimitedToStores =\u0026gt; new List\u0026lt;string\u0026gt;();\n    public IList\u0026lt;string\u0026gt; LimitedToGroups =\u0026gt; new List\u0026lt;string\u0026gt;();\n\n    public PaymentMethodType PaymentMethodType =\u0026gt; PaymentMethodType.Standard;\n\n    // InitPaymentTransaction, ProcessPayment, PostProcessPayment, PostRedirectPayment,\n    // HidePaymentMethod, GetAdditionalHandlingFee, Capture, Refund, Void, CancelPayment,\n    // SupportCapture, SupportRefund, SupportVoid ... see Payments.CashOnDelivery\n}\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003E\u003Ccode\u003EPaymentMethodType\u003C/code\u003E selects one of two flows. In the \u003Cem\u003Estandard\u003C/em\u003E flow the payment is completed while the order is placed and \u003Ccode\u003EProcessPayment\u003C/code\u003E returns the resulting transaction status. In the \u003Cem\u003Eredirection\u003C/em\u003E flow the order is placed as pending and \u003Ccode\u003EPostRedirectPayment\u003C/code\u003E returns the URL of the external payment page; the gateway\u0027s callback later marks the payment as paid. \u003Ccode\u003EPayments.CashOnDelivery\u003C/code\u003E is the simplest example (it leaves the payment pending), \u003Ccode\u003EPayments.StripeCheckout\u003C/code\u003E shows the redirection flow and \u003Ccode\u003EPayments.BrainTree\u003C/code\u003E collects card data in the checkout form.\u003C/p\u003E\n\n\u003Ch2 id=\u0022shipping\u0022\u003EShipping rate calculation: IShippingRateCalculationProvider\u003C/h2\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic interface IShippingRateCalculationProvider : IProvider\n{\n    ShippingRateCalculationType ShippingRateCalculationType { get; }\n    IShipmentTracker ShipmentTracker { get; }\n    Task\u0026lt;GetShippingOptionResponse\u0026gt; GetShippingOptions(GetShippingOptionRequest request);\n    Task\u0026lt;bool\u0026gt; HideShipmentMethods(IList\u0026lt;ShoppingCartItem\u0026gt; cart);\n    Task\u0026lt;double?\u0026gt; GetFixedRate(GetShippingOptionRequest request);\n    Task\u0026lt;IList\u0026lt;string\u0026gt;\u0026gt; ValidateShippingForm(string shippingOption, IDictionary\u0026lt;string, string\u0026gt; data);\n    Task\u0026lt;string\u0026gt; GetControllerRouteName();\n}\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003E\u003Ccode\u003EGetShippingOptions\u003C/code\u003E receives the cart items, the customer and the shipping address and returns the options with their rates. A minimal implementation:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic async Task\u0026lt;GetShippingOptionResponse\u0026gt; GetShippingOptions(GetShippingOptionRequest request)\n{\n    var response = new GetShippingOptionResponse();\n    if (request.Items == null || request.Items.Count == 0)\n    {\n        response.AddError(\u0022No shipment items\u0022);\n        return response;\n    }\n\n    response.ShippingOptions.Add(new ShippingOption {\n        Name = \u0022Courier\u0022,\n        Description = \u0022Delivered in 1-2 business days\u0022,\n        Rate = await _currencyService.ConvertFromPrimaryStoreCurrency(\n            _settings.Rate, _contextAccessor.WorkContext.WorkingCurrency)\n    });\n    return response;\n}\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003ERates are stored in the primary store currency and converted to the customer\u0027s working currency. \u003Ccode\u003EGetFixedRate\u003C/code\u003E lets the cart show an estimate before checkout; return \u003Ccode\u003Enull\u003C/code\u003E when the rate depends on the address. \u003Ccode\u003EGetControllerRouteName\u003C/code\u003E and \u003Ccode\u003EValidateShippingForm\u003C/code\u003E are for providers that show an extra form in checkout, such as a pickup point selector (\u003Ccode\u003EShipping.ShippingPoint\u003C/code\u003E). See also \u003Ccode\u003EShipping.FixedRateShipping\u003C/code\u003E and \u003Ccode\u003EShipping.ByWeight\u003C/code\u003E.\u003C/p\u003E\n\n\u003Ch2 id=\u0022widget\u0022\u003EWidgets: IWidgetProvider\u003C/h2\u003E\n\u003Cp\u003EStorefront views contain named widget zones, rendered with \u003Ccode\u003E@await Component.InvokeAsync(\u0022Widget\u0022, new { widgetZone = \u0022home_page_top\u0022 })\u003C/code\u003E. For each zone the core asks every active widget provider whether it wants to render there and which view component to invoke.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic class ExampleWidgetProvider(ITranslationService translationService) : IWidgetProvider\n{\n    public string ConfigurationUrl =\u0026gt; \u0022\u0022;\n    public string SystemName =\u0026gt; \u0022Widgets.Example\u0022;\n    public string FriendlyName =\u0026gt; translationService.GetResource(\u0022Widgets.Example.FriendlyName\u0022);\n    public int Priority =\u0026gt; 0;\n    public IList\u0026lt;string\u0026gt; LimitedToStores =\u0026gt; new List\u0026lt;string\u0026gt;();\n    public IList\u0026lt;string\u0026gt; LimitedToGroups =\u0026gt; new List\u0026lt;string\u0026gt;();\n\n    public Task\u0026lt;IList\u0026lt;string\u0026gt;\u0026gt; GetWidgetZones() =\u0026gt;\n        Task.FromResult\u0026lt;IList\u0026lt;string\u0026gt;\u0026gt;(new List\u0026lt;string\u0026gt; { \u0022body_end_html_tag_before\u0022 });\n\n    public Task\u0026lt;string\u0026gt; GetPublicViewComponentName(string widgetZone) =\u0026gt;\n        Task.FromResult(\u0022WidgetsExample\u0022);\n}\n\npublic class WidgetsExampleViewComponent : ViewComponent\n{\n    public IViewComponentResult Invoke(string widgetZone, object additionalData = null)\n        =\u0026gt; View(\u0022Default\u0022);\n}\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003EAfter installing, the administrator activates the widget under \u003Cstrong\u003EPlugins \u2192 Widgets\u003C/strong\u003E. Tracking widgets such as \u003Ccode\u003EWidgets.GoogleAnalytics\u003C/code\u003E and \u003Ccode\u003EWidgets.FacebookPixel\u003C/code\u003E check the customer\u0027s cookie consent before rendering their scripts; follow that pattern for anything that tracks visitors.\u003C/p\u003E\n\n\u003Ch2 id=\u0022discount-rules\u0022\u003EDiscount rules: IDiscountProvider and IDiscountRule\u003C/h2\u003E\n\u003Cp\u003EA discount rule plugin adds requirement types to the discount editor (\u003Cstrong\u003EMarketing \u2192 Discounts\u003C/strong\u003E, requirements of a discount). The provider returns the rules; each rule checks one requirement and points to its own configuration page.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic class ExampleDiscountProvider(MinimumCartRule minimumCartRule) : IDiscountProvider\n{\n    public string ConfigurationUrl =\u0026gt; \u0022\u0022;\n    public string SystemName =\u0026gt; \u0022DiscountRules.Example\u0022;\n    public string FriendlyName =\u0026gt; \u0022Example discount requirements\u0022;\n    public int Priority =\u0026gt; 0;\n    public IList\u0026lt;string\u0026gt; LimitedToStores =\u0026gt; new List\u0026lt;string\u0026gt;();\n    public IList\u0026lt;string\u0026gt; LimitedToGroups =\u0026gt; new List\u0026lt;string\u0026gt;();\n    public IList\u0026lt;IDiscountRule\u0026gt; GetRequirementRules() =\u0026gt; [minimumCartRule];\n}\n\npublic class MinimumCartRule : IDiscountRule\n{\n    public string SystemName =\u0026gt; \u0022DiscountRequirement.Example.MinimumItems\u0022;\n    public string FriendlyName =\u0026gt; \u0022Cart has at least N items\u0022;\n\n    public Task\u0026lt;DiscountRuleValidationResult\u0026gt; CheckRequirement(DiscountRuleValidationRequest request)\n    {\n        var result = new DiscountRuleValidationResult();\n        if (int.TryParse(request.DiscountRule.Metadata, out var minimum))\n            result.IsValid = request.Customer.ShoppingCartItems.Count \u0026gt;= minimum;\n        return Task.FromResult(result);\n    }\n\n    public string GetConfigurationUrl(string discountId, string discountRequirementId) =\u0026gt;\n        $\u0022/DiscountRulesExample/Configure/?discountId=\u0022 \u002B discountId\n        \u002B (string.IsNullOrEmpty(discountRequirementId) ? \u0022\u0022 : \u0022\u0026amp;discountRequirementId=\u0022 \u002B discountRequirementId);\n}\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003EThe configured value of a requirement is stored as a string in \u003Ccode\u003EDiscountRule.Metadata\u003C/code\u003E; the rule\u0027s configuration controller writes it and \u003Ccode\u003ECheckRequirement\u003C/code\u003E reads it. Register the provider and every rule in \u003Ccode\u003EStartupApplication\u003C/code\u003E (\u003Ccode\u003EAddScoped\u0026lt;IDiscountProvider, ExampleDiscountProvider\u0026gt;()\u003C/code\u003E and \u003Ccode\u003EAddScoped\u0026lt;MinimumCartRule\u0026gt;()\u003C/code\u003E). \u003Ccode\u003EDiscountRules.Standard\u003C/code\u003E ships five rules \u2014 customer group, amount spent, has all products, has one product and cart subtotal \u2014 and is the reference.\u003C/p\u003E\n\n\u003Ch2 id=\u0022other-types\u0022\u003EOther provider types\u003C/h2\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003EInterface\u003C/th\u003E\u003Cth\u003EUsed for\u003C/th\u003E\u003Cth\u003EShipped example\u003C/th\u003E\u003C/tr\u003E\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EITaxProvider\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003ETax rate calculation\u003C/td\u003E\u003Ctd\u003E\u003Ccode\u003ETax.FixedRate\u003C/code\u003E, \u003Ccode\u003ETax.CountryStateZip\u003C/code\u003E\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EIExternalAuthenticationProvider\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003ELogin with an external account\u003C/td\u003E\u003Ctd\u003E\u003Ccode\u003EAuthentication.Google\u003C/code\u003E, \u003Ccode\u003EAuthentication.Facebook\u003C/code\u003E\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EIExchangeRateProvider\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003ECurrency exchange rates\u003C/td\u003E\u003Ctd\u003E\u003Ccode\u003EExchangeRate.McExchange\u003C/code\u003E\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EIThemeView\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EStorefront themes\u003C/td\u003E\u003Ctd\u003E\u003Ccode\u003ETheme.Modern\u003C/code\u003E \u2014 see \u003Ca href=\u0022/developers-creating-a-theme\u0022\u003ECreating a theme\u003C/a\u003E\u003C/td\u003E\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\n\n\u003Ch2 id=\u0022related\u0022\u003ERelated\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/developers-writing-a-plugin\u0022\u003EWriting a plugin\u003C/a\u003E\u003C/li\u003E\n\u003Cli\u003EConfiguring payment and shipping methods as an operator: \u003Ca href=\u0022/docs-configuration\u0022\u003EConfiguration\u003C/a\u003E\u003C/li\u003E\n\u003C/ul\u003E","ParentCategoryId":"6abdec6d83d2816248229f65","SeName":"developers-plugin-provider-types","MetaKeywords":null,"MetaDescription":"Provider interfaces a GrandNode plugin implements: IPaymentProvider, IShippingRateCalculationProvider, IWidgetProvider and discount rules.","MetaTitle":null,"AllowComments":false,"Captcha":{"ReCaptchaChallengeField":null,"ReCaptchaResponseField":null,"ReCaptchaResponseValue":null,"ReCaptchaResponse":null},"RelatedArticles":[],"CategoryBreadcrumb":[{"Name":"For developers","Description":null,"IsCurrent":false,"Children":null,"Parent":null,"SeName":"docs-developers","Id":"6abdec6d83d2816248229f65","UserFields":[]}],"AddNewComment":{"CommentText":null,"DisplayCaptcha":false,"Id":null,"UserFields":[]},"Comments":[],"Id":"6abdec6d83d2816248229f6d","UserFields":[]}