Development
Theme development
Build a theme for the checkout, invoice and payment-link pages.
A theme controls how customers see a Brand's checkout, invoice and payment-link pages. Paybob handles everything behind those pages: which gateways are offered, prices, form validation and payments. Your theme only decides how they look.
Package structure
themes/midnight/
├── ProcessTheme.php ← required: the theme class
├── assets/
│ ├── preview.svg ← the preview shown in the theme gallery
│ ├── style.css
│ └── script.js
└── views/
├── checkout.blade.php ← required
├── invoice.blade.php ← required
├── payment-link.blade.php ← required
├── payment-link-default.blade.php← required
├── layout.blade.php ← optional: your shared layout
└── partials/ ← optionalThe folder name is the theme's slug: lowercase letters, numbers, hyphens and underscores, starting with a letter or number, up to 80 characters.
The theme class
ProcessTheme.php declares ProcessTheme in the namespace Themes\{StudlySlug}Theme, extending Paybob's Theme class:
<?php
namespace Themes\MidnightTheme;
use App\Support\Themes\Forms\Field;
use App\Support\Themes\Forms\Form;
use App\Support\Themes\Forms\Section;
use App\Support\Themes\Theme;
use App\Support\Themes\ThemeMetadata;
class ProcessTheme extends Theme
{
public static function metadata(): ThemeMetadata
{
return ThemeMetadata::make()
->name('Midnight')
->logo('assets/preview.svg')
->version('1.0.0');
}
public function config(): Form
{
return Form::make([
Section::make('Colours')->schema([
Field::make('primary_color')->color()->label('Primary color')->default('#146EF5')->required(),
Field::make('show_faq')->toggle()->label('Show FAQ section')->default(true),
]),
]);
}
}For themes/midnight/, the namespace is Themes\MidnightTheme.
The four pages
| Page | View | Shown at |
|---|---|---|
| Checkout: the customer picks a gateway | checkout | /{brand}/checkout/{t_id} |
| Invoice: view and pay an invoice | invoice | /{brand}/invoice/{i_id} |
| Payment link | payment-link | /{brand}/paymentlink/{p_id} |
| Default payment link: the customer enters an amount | payment-link-default | /{brand}/paymentlink/default/{currency} |
All four views must exist. To use a different view for a page, override checkout(), invoice(), paymentlink() or paymentlinkdefault() and return the name of another view in your package.
Your views are in their own namespace. Every view gets $themeNamespace, so you can extend and include without hard-coding the slug:
@extends($themeNamespace.'::layout')
@include($themeNamespace.'::partials.header')Settings
config() returns the form admins see under Customize. Settings are saved per theme and per Brand, and kept if the Brand switches themes and back.
| Field type | |
|---|---|
text(), textarea(), number() | Text and numbers |
select($options), radio($options) | Choices, as value => label |
toggle(), checkbox() | On/off |
color() | A colour, as a 6-digit hex value |
image() | An image upload (PNG, JPEG, WebP or GIF, up to 2 MB) |
password() | A secret, encrypted and never passed to your public views |
Modifiers: label(), placeholder(), helperText(), default(), required(), nullable(), disabled(), readOnly(), revealable(), columnSpan(), columnSpanFull(), and rules([...]) for extra validation (min, max, size, email, url, integer, numeric, alpha_dash, starts_with, ends_with). Keys are lowercase snake_case. Section::make('Title')->schema([...]) groups fields; sections are optional.
Read settings in views from $themeSettings['primary_color'], or with theme_setting('primary_color', '#146EF5'). Settings are plain text, not HTML: output them with {{ }}, never {!! !!}.
What each page receives
Every view gets:
| Variable | Contains |
|---|---|
$context | The page's data (below). Read-only. |
$context->merchant | The Brand's profile: name, logo, favicon, address, support contacts, social links, SEO, Tag Manager ID. Every value is display-ready. |
$context->faq | The Brand's active FAQs, each with title and content. |
$themeSettings | Your theme's settings. |
$theme | Your theme's metadata. |
$locale | The customer's language. |
Checkout
| Method | Gives |
|---|---|
$context->transaction() | t_id, amount, discount, processing_fee, total, status, payment_method, redirect_url |
$context->customer() | name, email, phone |
$context->gateways() | Every gateway available for this payment: g_id, name, logo_url, currency, amount, discount, processing_fee, total, min_amount, max_amount, checkout_url |
$context->menus() | The Brand's Checkout Menus, in order, each with title, icon and its gateways. Empty when the Brand has none: then show gateways() as one list. |
$context->currency() | The payment's currency |
Each gateway's prices are already converted and include its fees and discounts. Link each gateway to its checkout_url. A menu can be empty: whether to show it is up to you.
When a transaction is already paid, show its status instead of the gateway list.
Invoice
$context->invoice() gives i_id, status, notes, due_date, overdue, items (each with description, quantity, amount, discount, vat, total) and totals (subtotal, discount, vat, shipping, grand_total), plus customer() and currency().
Payment links
$context->paymentLink() gives p_id, name, description, quantity, amount and image_url (the default link has none). $context->fields() lists the form fields to show: name, email, phone, the amount (default link only) and the link's custom fields. Each field has key, label, type, required and value, and may have readonly, options and file_extensions. Name custom fields custom[field_key].
Payment forms
The invoice and payment-link pages submit a form to start the payment:
@if ($context->submitUrl())
<form method="POST" action="{{ $context->submitUrl() }}" enctype="multipart/form-data">
@csrf
<input type="hidden" name="checkout_token" value="{{ $context->submitToken() }}">
{{-- your fields --}}
<button type="submit">Pay</button>
</form>
@elseif ($context->paymentUnavailable())
<p>Payments are temporarily unavailable. Please try again later.</p>
@endif- Only show the pay button when
submitUrl()is set. It isn't on a paid invoice, for example. - When
paymentUnavailable()is true, show that neutral message instead. Never tell customers why. - Include
@csrfand the hiddencheckout_token. - Use Laravel's
$errorsandold()to show validation errors, but never refill a read-only amount fromold().
Paybob checks everything again on the server: the amount, the currency and the customer always come from the invoice or link, never from the form.
Languages
Customers can choose English, Bangla, Hindi or Urdu, and their choice is shared with gateway pages. Build a language menu from customer_locale()->supported(), linking each option to ?lang={code}, and translate your own text with customer_locale()->resolve(['en' => 'Pay', 'bn' => 'পরিশোধ করুন']). Text the merchant typed (menu titles, field labels, FAQs) is shown exactly as entered.
Tag Manager and SEO
Read the Brand's settings from $context->merchant: googleTagManagerId(), metaDescription(), seoKeywords(), ogImageUrl() and twitterImageUrl(). Each is null or empty when not set: leave the tag out entirely rather than outputting an empty one.
Assets
Put CSS, JavaScript and images in assets/ and link them with theme_assets('assets/style.css'). Paybob publishes them when the theme is activated, and adds your theme's version to every URL, such as ?v=1.0.0.
Bump the version when assets change
Browsers keep cached copies of your CSS and JavaScript until the URL changes. Raise version() every time you change a file in assets/, or customers keep seeing the old one.
Testing and packaging
- Put the folder in
themes/on a development copy of Paybob. It appears on the Themes page; activate it to see it. - If your theme fails to render, Paybob falls back to the default theme and logs the error, so a broken theme never takes checkout down. Check the log while developing.
- ZIP the folder so the ZIP contains one top-level folder named after the slug (up to 50 MB), and install it with Themes → Upload Theme.
- Themes can't be updated by uploading yet: a ZIP whose slug is already installed is refused.
The default theme, themes/default/, is a complete example: grouped gateway menus, a support drawer with FAQs, four languages, colour settings and an invoice with "Download PDF".