From a paid invoice to a license key
- PHP
- HTML
- CSS
- JavaScript
- Invision Community
klar.gg sells software licenses through an Invision Community 4 forum. Before this project the store ran on stock Invision Commerce. Stock Commerce can sell a product and even generate a license key for it, but that key is a random string Invision made up. klar's software checks keys against klar's own panel, which has never heard of it.
So I wrote a plugin, klar.gg panel, and reworked the store theme around it. It does two things: sell products by duration instead of as one fixed item, and issue real panel keys the moment someone pays.
Keys on payment
Every Commerce invoice goes through Invoice::markPaid() once the money clears. The plugin hooks that method, lets Invision finish its own work first, and then looks at what was bought:
public function markPaid( \IPS\Member $member = NULL )
{
$return = parent::markPaid( $member );
foreach ( $this->items as $item )
{
// Only store products, and only ones mapped to a panel product
$panelProductName = $productMap[ (string) $item->id ] ?? NULL;
if ( !$panelProductName ) continue;
// ...resolve the duration, ask the panel for keys, attach them, email them
}
return $return;
}
Calling parent::markPaid() first matters. The purchases the keys attach to don't exist until Invision creates them, and if anything on klar's side fails, the customer still gets a paid invoice and a purchase. Staff can fix a missing key. A payment that silently didn't go through is much worse.
For each product, the plugin:
- Looks up which panel product it maps to. Each Commerce product gets a "Panel Product Name" field in the ACP, saved to a
product_mapsetting. - Reads the duration the customer picked (more on that below) and turns it into hours.
- Fetches the panel's
/productslist once per invoice and resolves the name to a panel ID. - Finds the purchases from this invoice that don't have a key yet, and asks the panel's
/keysendpoint for exactly that many in one request. - Saves each key as a standard Commerce license key on its purchase, so it shows up on the customer's purchase page like any other key.
- Sends one email per key, built from an HTML template staff can edit in the plugin settings.
Step 4's "don't have a key yet" check makes the hook safe to run twice. An invoice that gets marked paid again, or that a staff member nudges by hand, won't mint a second set of keys.
The email template supports placeholders like {productName}, {durationLabel}, {key}, and {purchaseSlug}, which builds a direct link to the order page. There's also {clientAreaContent}, which pulls in the product's own client-area text (setup steps, download links) so each email carries the instructions for what was bought. Everything that comes from user-editable data is HTML-escaped before it's dropped into the template.
Every step logs to an Invision log channel, klar_panel. When a customer says "I paid and got nothing," staff can see exactly which step stopped and why: no mapping, no duration, the panel was unreachable, or the panel returned fewer keys than asked for.
Selling by duration
A klar license isn't one product. It's the same product for a day, a week, or a month, at different prices. Stock Commerce can do that with custom fields and price adjustments, but the panel needs a number of hours, not a label someone typed.
The plugin adds a new custom field type, Duration. Each option is written as Label:Hours:
1 Day:24
1 Week:168
1 Month:720
The customer only ever sees the label. When the invoice is paid, markPaid() splits the chosen option on the first colon and sends the hours to the panel. Duration fields also work with Commerce's per-option pricing and stock, so each length gets its own price and its own stock count.
The product page was rebuilt around this. The stock purchase form is replaced with a "Select Options" card listing each duration with its price and stock. Options that are free or have a broken price are left out, so a half-configured option can't be bought for nothing.
Products that can't issue a key
The worst outcome is a customer paying for something that has no way to produce a key. So the product page checks before it offers the buy button. If a product has a Duration field but no panel mapping, or the panel can't be reached to confirm the product exists, the button is replaced with a "Product Ineligible for purchase" stamp. Misconfigured products fail on the product page, not after the payment.
What this gets you
A customer picks a duration, pays, and has a working key in their inbox and on their purchase page without anyone on the team touching it. Products that can't issue a key can't be bought, and when something does go wrong, the log says exactly where.