Skip to content

Let store owners configure payment plugins per store - #840

Merged
KrzysztofPajak merged 3 commits into
developfrom
feature/store-owner-payment-plugin-config
Sep 13, 2026
Merged

KrzysztofPajak merged 3 commits into
developfrom
feature/store-owner-payment-plugin-config

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Sep 13, 2026

Copy link
Copy Markdown
Member

Type: feature

Issue

A store owner can activate a payment method in the Store panel but cannot configure any of them. The grid at /Store/Payment/Index renders the method's friendly name and system name as plain text, so there is no way into the plugin's settings. Two things stand in the way:

  1. The shipped payment plugins (Payments.CashOnDelivery, Payments.BrainTree, Payments.StripeCheckout) only have Areas/Admin controllers, and their ConfigurationUrl is an absolute /Admin/... path — linking to it would send a store manager into the admin area rather than to a screen they are allowed to use.
  2. Two plugins were not correctly per-store even for an admin. BrainTree's admin screen read its settings from the injected instance (resolved for whichever store hosts the admin panel) and saved them with no store scope, so the store scope selector was ignored and per-store credentials were impossible. StripeCheckoutService assigned the api key to the process-wide static StripeConfiguration.ApiKey; harmless with one key for all stores, but with a key per store two concurrent checkouts overwrite each other's credentials.

Reproduce: sign in to /Store as a store manager with ManagePaymentMethods granted, open Payment methods — the names are not links, and there is no other route to the plugin settings.

Solution

Each payment plugin now ships an Areas/Store configuration screen that loads and saves its settings for the manager's own store (StaffStoreId), gateway credentials included, so each store transacts on its own Stripe/BrainTree account. The screens reuse the plugins' existing configuration models and localization resources — no new resources, no migration.

The Store grid links to that screen. The url is built from the Store-area controller the plugin actually ships (StoreAreaConfiguration.GetConfigurationUrl) rather than from the provider's own ConfigurationUrl, since the latter points at the admin area; a plugin without a Store screen renders plain text, exactly as the Shipping grid already does.

Storefront behavior needed no change: AddSettings already registers every ISettings class as scoped and loads it for the current store, so checkout has always read per-store values. The two genuine defects above are fixed — BrainTree's admin screen now honors the selected store scope (and offers the selector), and the Stripe api key travels with a per-call StripeClient.

Breaking changes

None to public contracts. One behavior change worth noting: BrainTree's admin configuration screen now writes to the selected store scope instead of always writing globally. With the scope set to all stores (the default) the behavior is identical; an admin who had a specific store selected will now see their edits apply to that store, which is the point of the fix.

Note that StoreManager does not carry ManagePaymentMethods by default — an admin must grant it before the menu entry and these screens appear. This is pre-existing and unchanged here.

Testing

  1. As a main admin, grant the StoreManager customer group the ManagePaymentMethods permission, and assign a store manager customer to a store.
  2. Sign in to /Store as that store manager and open Payment methods. The friendly name and system name of Cash on delivery, BrainTree and Stripe are now links; a payment plugin without a Store-area screen would stay plain text.
  3. Click Cash on delivery. Change the additional fee and save. Confirm the success notification and that the value persists on reload.
  4. In the main admin panel, open the same plugin with the store scope set to all stores. Confirm the global value is unchanged — the store manager's save created a store-scoped row only.
  5. Place an order in that store's storefront and confirm the additional fee from step 3 is applied, then place one in another store and confirm it is not.
  6. Open Stripe in the Store panel. Confirm the webhook url shown is that of the manager's own store, not of the host serving the panel. Fill in a test api key and secret, save, and confirm a checkout in that store uses them.
  7. As a main admin, open BrainTree, switch the store scope selector to a specific store, save credentials, then switch back to all stores and confirm the global credentials were not overwritten.
  8. dotnet build GrandNode.sln and dotnet test src/Tests/Grand.Web.Store.Tests both pass.

KrzysztofPajak and others added 2 commits September 13, 2026 12:47
The Store payment grid rendered method names as plain text, so a store
manager could activate a payment method but never reach its settings.
The shipped payment plugins only had Areas/Admin controllers, and their
ConfigurationUrl is an absolute /Admin/... path, so linking to it would
have sent the manager into the admin area instead.

Each payment plugin now ships an Areas/Store configuration screen that
reads and writes its settings for the manager's own store, and the grid
links to that screen - built from the Store-area controller the plugin
actually ships, so a plugin without one still renders plain text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bf2UUL1avH6UD5Mxxzu7UK
BrainTree's admin screen wrote its settings globally and read them from
the injected instance, which is resolved for whichever store hosts the
admin panel - the store scope selector was ignored, so per-store
credentials were impossible. It now loads and saves for the selected
scope, like the other payment plugins, and offers the scope selector.

Stripe assigned the api key to the process-wide StripeConfiguration
before every call. With one key for all stores that was harmless; with a
key per store two concurrent checkouts would overwrite each other's
credentials, so the key now travels with a per-call StripeClient.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bf2UUL1avH6UD5Mxxzu7UK
Copilot AI lite review requested due to automatic review settings September 13, 2026 10:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The Store-area controller the StoreAreaConfiguration tests discover is a stand-in
for a shipped plugin configuration screen, but its POST overload lacked the
antiforgery attribute every real one carries, which CodeQL flagged as a missing
CSRF token validation. Mirror the shipped controllers so the stand-in is faithful.
@KrzysztofPajak
KrzysztofPajak merged commit 29f3a81 into develop Sep 13, 2026
6 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the feature/store-owner-payment-plugin-config branch September 13, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants