Jump to content

All Activity

This stream auto-updates

  1. Today
  2. Hi, I am wanting to create a system in my WHMCS where specific clients can access digital downloads in their client area. I only want it available to clients that either purchase each digital download or if they purchase a specific package. Is this something that I can do? Or is already available?
  3. This is a problem with WHMCS 7.8.3, not a patch that isn't there. GoCardless changed the format of their redirect URL, and older versions of WHMCS won't accept it because it says "invalid filename." You can't backport module-7849. The only way to really fix it is to upgrade WHMCS to 8.5.2 or higher, or stop using GoCardless. Any core hack on 7.8.3 will be weak and not supported.
  4. Yesterday
  5. Hi All, I put together a small WHMCS Admin Home Widget that summarises profit/loss from your WHMCS transactions and presents it in your base currency, including UK tax-year reporting and a simple toggle for invoice-only vs all transactions. I noticed that there was no way to do this in WHMCS as it uses January-December as the year. What it shows The widget displays totals (base currency) for: This Month (month-to-date) Previous Month (full month) Tax Year to Date (UK tax year: 6 Apr → 5 Apr) Last Year’s Tax YTD (same “elapsed point” comparison) It also shows an “Average per month” row for Income / Expenses / Profit: This Month: month-to-date totals (shown as “per month” for consistency; updates daily) Previous Month: totals for the full month Tax YTD: divided by tax months elapsed (UK tax months run 6th → 5th) Last Year’s Tax YTD: calculated as a full-year average (÷ 12) (so you get a completed-year monthly average) How the numbers are calculated It pulls data from tblaccounts and uses: Income = Amount In Expenses = Fees + Amount Out Profit = Amount In − Fees − Amount Out Multi-currency handling WHMCS stores transactions per client currency; the widget groups by currency and converts everything back into your base currency using tblcurrencies.rate, then displays base totals. Mode toggle (Invoice-linked vs All transactions) At the top of the widget there’s a mode toggle: Invoice-linked: includes only transactions linked to an invoice (invoiceid > 0) Usually represents customer payments/refunds + gateway fees. All transactions: includes everything in tblaccounts (including manual entries / “Amount Out” etc.) The selection persists via a cookie/session (pl_mode) and the widget is set to no cache so switching updates instantly. UK tax year behaviour UK tax year is 6 April to 5 April The widget calculates Tax YTD from the current tax-year start to “now” For comparison, “Last Year’s Tax YTD” uses the same elapsed point into the previous tax year (e.g., if we’re 10 tax months into the current year, it compares the first 10 tax months of last year) The “tax months elapsed” counter follows UK tax-month boundaries (6th → 5th), so the elapsed count can change on the 6th of each month Installation Create a file like: whmcs/includes/hooks/profit_loss_widget.php Paste the code in. Visit the WHMCS admin dashboard (Home). You can drag/reorder widgets as normal. Code Is Below: <?php if (!defined("WHMCS")) { die("No direct access"); } use WHMCS\Database\Capsule; use Carbon\Carbon; add_hook('AdminHomeWidgets', 1, function () { return new class extends \WHMCS\Module\AbstractWidget { protected $title = 'Profit / Loss (Month, Prev Month, UK Tax Year)'; protected $description = 'Base-currency totals only. Amount In − Fees − Amount Out. UK tax year 6 Apr – 5 Apr.'; protected $weight = 150; protected $columns = 2; // wider widget (set 1/2/3 to suit your dashboard) protected $cache = false; protected $cacheExpiry = 0; protected $requiredPermission = ''; public function getData() { try { $now = Carbon::now(); // Mode: 'invoice' (payments & refunds only) or 'all' (includes withdrawals/expenditures) $mode = $this->getMode(); $onlyInvoiceLinked = ($mode === 'invoice'); $opts = ['onlyInvoiceLinked' => $onlyInvoiceLinked]; // Calendar months $startMonth = $now->copy()->startOfMonth(); $prevMonthStart = $now->copy()->subMonth()->startOfMonth(); $prevMonthEnd = $now->copy()->subMonth()->endOfMonth(); // UK tax year (6 Apr – 5 Apr) $tyStart = $this->ukTaxYearStart($now); $tyEnd = $tyStart->copy()->addYear()->subDay()->endOfDay(); // 5 Apr end-of-day // Last tax year (full) $lastTyStart = $tyStart->copy()->subYear(); $lastTyEnd = $lastTyStart->copy()->addYear()->subDay()->endOfDay(); // 5 Apr end-of-day // Query per-currency then convert to base totals $monthRows = $this->sumProfitByCurrency($startMonth, $now, $opts); // this month-to-date $prevMonthRows = $this->sumProfitByCurrency($prevMonthStart, $prevMonthEnd, $opts); // full previous month // Current tax year is "to date" (can’t include future) $taxYearToDateRows = $this->sumProfitByCurrency($tyStart, $now, $opts); // Last tax year is full 12 months $lastTaxYearFullRows = $this->sumProfitByCurrency($lastTyStart, $lastTyEnd, $opts); $base = Capsule::table('tblcurrencies')->where('default', 1)->first(); // --- Averages per month (income, expenses, profit) --- // Month periods: treat as 1 month each (this month-to-date still counts as a month per your request) $monthTotalsMulti = $this->toBaseTotalsMulti($monthRows, $base); $prevMonthTotalsMulti = $this->toBaseTotalsMulti($prevMonthRows, $base); $monthAvg = [ 'income' => $this->avgPerMonth($monthTotalsMulti['amount_in'], 1), 'expenses' => $this->avgPerMonth($monthTotalsMulti['fees'] + $monthTotalsMulti['amount_out'], 1), 'profit' => $this->avgPerMonth($monthTotalsMulti['profit'], 1), 'months' => 1, ]; $prevMonthAvg = [ 'income' => $this->avgPerMonth($prevMonthTotalsMulti['amount_in'], 1), 'expenses' => $this->avgPerMonth($prevMonthTotalsMulti['fees'] + $prevMonthTotalsMulti['amount_out'], 1), 'profit' => $this->avgPerMonth($prevMonthTotalsMulti['profit'], 1), 'months' => 1, ]; // Current tax year average uses UK tax-months elapsed (6th → 5th) $tyMonths = $this->ukTaxMonthsElapsed($tyStart, $now); $taxYearTotalsMulti = $this->toBaseTotalsMulti($taxYearToDateRows, $base); $lastTaxYearTotalsMulti = $this->toBaseTotalsMulti($lastTaxYearFullRows, $base); $taxYearAvg = [ 'income' => $this->avgPerMonth($taxYearTotalsMulti['amount_in'], $tyMonths), 'expenses' => $this->avgPerMonth($taxYearTotalsMulti['fees'] + $taxYearTotalsMulti['amount_out'], $tyMonths), 'profit' => $this->avgPerMonth($taxYearTotalsMulti['profit'], $tyMonths), 'months' => $tyMonths, ]; // Last tax year is a completed year: always 12 months $lastTaxYearAvg = [ 'income' => $this->avgPerMonth($lastTaxYearTotalsMulti['amount_in'], 12), 'expenses' => $this->avgPerMonth($lastTaxYearTotalsMulti['fees'] + $lastTaxYearTotalsMulti['amount_out'], 12), 'profit' => $this->avgPerMonth($lastTaxYearTotalsMulti['profit'], 12), 'months' => 12, ]; // --- /Averages --- return [ 'totals' => [ 'month' => $this->toBaseTotal($monthRows, $base), 'prev_month' => $this->toBaseTotal($prevMonthRows, $base), 'tax_year' => $this->toBaseTotal($taxYearToDateRows, $base), // current tax year to date 'last_tax_year' => $this->toBaseTotal($lastTaxYearFullRows, $base), // last tax year full 'base' => $base ? (array)$base : ['code'=>'','prefix'=>'','suffix'=>''], ], 'averages' => [ 'month' => $monthAvg, 'prev_month' => $prevMonthAvg, 'tax_year' => $taxYearAvg, 'last_tax_year' => $lastTaxYearAvg, ], 'meta' => [ 'month_label' => $startMonth->format('M Y'), 'prev_month_label' => $prevMonthStart->format('M Y'), 'tax_year_label' => $this->formatTaxYearLabel($tyStart), 'tax_year_full_range' => $tyStart->format('j M Y') . ' – ' . $tyEnd->format('j M Y'), 'tax_year_asof' => $now->format('j M Y'), 'last_tax_year_label' => $this->formatTaxYearLabel($lastTyStart), 'last_tax_year_full_range' => $lastTyStart->format('j M Y') . ' – ' . $lastTyEnd->format('j M Y'), 'generated' => $now->toDayDateTimeString(), 'mode' => $mode, // 'invoice' or 'all' ], ]; } catch (\Throwable $e) { return ['error' => $e->getMessage(), 'meta' => ['generated' => date('r')]]; } } /** Persist/return the current mode (invoice|all) using GET → session/cookie. */ private function getMode() { $valid = ['invoice', 'all']; $get = isset($_GET['pl_mode']) ? strtolower((string)$_GET['pl_mode']) : null; if ($get && in_array($get, $valid, true)) { if (isset($_SESSION)) { $_SESSION['pl_mode'] = $get; } @setcookie('pl_mode', $get, time() + 31536000, '/'); // 1 year return $get; } if (isset($_SESSION['pl_mode']) && in_array($_SESSION['pl_mode'], $valid, true)) { return $_SESSION['pl_mode']; } if (isset($_COOKIE['pl_mode']) && in_array($_COOKIE['pl_mode'], $valid, true)) { return $_COOKIE['pl_mode']; } return 'invoice'; } /** Build a link to switch mode while preserving the current path & query. */ private function toggleUrl($toMode) { $uri = $_SERVER['REQUEST_URI'] ?? 'index.php'; $parts = parse_url($uri); $path = $parts['path'] ?? 'index.php'; $q = []; if (!empty($parts['query'])) { parse_str($parts['query'], $q); } $q['pl_mode'] = $toMode; $qs = http_build_query($q); return $path . ($qs ? '?' . $qs : ''); } private function ukTaxYearStart(Carbon $date) { $tyStart = Carbon::create($date->year, 4, 6, 0, 0, 0, $date->timezone); if ($date->lt($tyStart)) { $tyStart->subYear(); } return $tyStart->startOfDay(); } private function formatTaxYearLabel(Carbon $tyStart) { $startYear = (int)$tyStart->format('Y'); $endYY = (int)$tyStart->copy()->addYear()->format('y'); return sprintf('%d/%02d', $startYear, $endYY); } /** * UK tax months elapsed in the tax year (tax months run 6th → 5th). * Returns an integer 1..12. */ private function ukTaxMonthsElapsed(Carbon $tyStart, Carbon $to) { if ($to->lt($tyStart)) { return 0; } $startY = (int)$tyStart->format('Y'); $startM = 4; // tax year starts in April $toY = (int)$to->format('Y'); $toM = (int)$to->format('n'); $toD = (int)$to->format('j'); $diffMonths = (($toY - $startY) * 12) + ($toM - $startM); $monthsElapsed = $diffMonths + (($toD >= 6) ? 1 : 0); if ($monthsElapsed < 1) { $monthsElapsed = 1; } if ($monthsElapsed > 12) { $monthsElapsed = 12; } return $monthsElapsed; } /** Aggregate per currency for a period; optionally invoice-linked only. */ private function sumProfitByCurrency(Carbon $from, Carbon $to, array $opts = []) { $onlyInvoices = !empty($opts['onlyInvoiceLinked']); $q = Capsule::table('tblaccounts as a') ->leftJoin('tblclients as cl', 'cl.id', '=', 'a.userid') ->leftJoin('tblcurrencies as cur', 'cur.id', '=', 'cl.currency') ->whereBetween('a.date', [$from->toDateTimeString(), $to->toDateTimeString()]); if ($onlyInvoices) { $q->where('a.invoiceid', '>', 0); } $rows = $q->groupBy('cl.currency', 'cur.code', 'cur.prefix', 'cur.suffix', 'cur.rate') ->orderBy('cur.code') ->get([ Capsule::raw('cl.currency as currencyId'), Capsule::raw('cur.code as code'), Capsule::raw('cur.prefix as prefix'), Capsule::raw('cur.suffix as suffix'), Capsule::raw('cur.rate as rate'), Capsule::raw('SUM(a.amountin) as amount_in'), Capsule::raw('SUM(a.fees) as fees'), Capsule::raw('SUM(a.amountout) as amount_out'), Capsule::raw('SUM(a.amountin - a.fees - a.amountout) as profit'), ]); $defaultCurrency = Capsule::table('tblcurrencies')->where('default', 1)->first(); return $rows->map(function ($r) use ($defaultCurrency) { $code = $r->code ?? ($defaultCurrency->code ?? 'BASE'); $prefix = $r->prefix ?? ($defaultCurrency->prefix ?? ''); $suffix = $r->suffix ?? ($defaultCurrency->suffix ?? ''); $rate = isset($r->rate) ? (float)$r->rate : (float)($defaultCurrency->rate ?? 1.0); $cid = isset($r->currencyId) ? (int)$r->currencyId : (int)($defaultCurrency->id ?? 0); return [ 'currencyId' => $cid, 'code' => (string)$code, 'prefix' => (string)$prefix, 'suffix' => (string)$suffix, 'rate' => (float)$rate, 'amount_in' => (float)$r->amount_in, 'fees' => (float)$r->fees, 'amount_out' => (float)$r->amount_out, 'profit' => (float)$r->profit, ]; })->all(); } /** Convert to base-currency total (profit only). */ private function toBaseTotal(array $rows, $baseCurrency) { if (!$baseCurrency) { return ['amount' => 0.0, 'prefix' => '', 'suffix' => '', 'code' => '']; } $baseId = (int)$baseCurrency->id; $sum = 0.0; foreach ($rows as $r) { $profit = (float)$r['profit']; $sum += ((int)$r['currencyId'] === $baseId) ? $profit : ($profit / ((float)$r['rate'] ?: 1.0)); } return [ 'amount' => $sum, 'prefix' => (string)$baseCurrency->prefix, 'suffix' => (string)$baseCurrency->suffix, 'code' => (string)$baseCurrency->code, ]; } /** Convert rows to base totals for multiple fields (amount_in, fees, amount_out, profit). */ private function toBaseTotalsMulti(array $rows, $baseCurrency) { if (!$baseCurrency) { return [ 'amount_in' => 0.0, 'fees' => 0.0, 'amount_out' => 0.0, 'profit' => 0.0, ]; } $baseId = (int)$baseCurrency->id; $totals = [ 'amount_in' => 0.0, 'fees' => 0.0, 'amount_out' => 0.0, 'profit' => 0.0, ]; foreach ($rows as $r) { $rate = (float)($r['rate'] ?? 1.0); if ($rate == 0.0) { $rate = 1.0; } $isBase = ((int)$r['currencyId'] === $baseId); $conv = function ($v) use ($isBase, $rate) { $v = (float)$v; return $isBase ? $v : ($v / $rate); }; $totals['amount_in'] += $conv($r['amount_in'] ?? 0); $totals['fees'] += $conv($r['fees'] ?? 0); $totals['amount_out'] += $conv($r['amount_out'] ?? 0); $totals['profit'] += $conv($r['profit'] ?? 0); } return $totals; } /** Safe average helper. */ private function avgPerMonth($total, $months) { $months = (int)$months; if ($months <= 0) { return 0.0; } return (float)$total / $months; } public function generateOutput($data) { if (!empty($data['error'])) { return '<div class="widget-content-padded"><strong>Error:</strong> ' . htmlspecialchars($data['error'], ENT_QUOTES, 'UTF-8') . '<br><small>Generated ' . htmlspecialchars($data['meta']['generated'], ENT_QUOTES, 'UTF-8') . '</small></div>'; } $fmt = function ($prefix, $amount, $suffix) { return sprintf('%s%s%s', htmlspecialchars($prefix ?? '', ENT_QUOTES, 'UTF-8'), number_format((float)$amount, 2), htmlspecialchars($suffix ?? '', ENT_QUOTES, 'UTF-8') ); }; $t = $data['totals']; $m = $data['meta']; $avg = $data['averages'] ?? []; $monthBase = $fmt($t['base']['prefix'] ?? '', $t['month']['amount'] ?? 0, $t['base']['suffix'] ?? ''); $prevMonthBase = $fmt($t['base']['prefix'] ?? '', $t['prev_month']['amount'] ?? 0, $t['base']['suffix'] ?? ''); $tyBase = $fmt($t['base']['prefix'] ?? '', $t['tax_year']['amount'] ?? 0, $t['base']['suffix'] ?? ''); $lastTyBase = $fmt($t['base']['prefix'] ?? '', $t['last_tax_year']['amount'] ?? 0, $t['base']['suffix'] ?? ''); $mAvgIncome = $fmt($t['base']['prefix'] ?? '', $avg['month']['income'] ?? 0, $t['base']['suffix'] ?? ''); $mAvgExpenses = $fmt($t['base']['prefix'] ?? '', $avg['month']['expenses'] ?? 0, $t['base']['suffix'] ?? ''); $mAvgProfit = $fmt($t['base']['prefix'] ?? '', $avg['month']['profit'] ?? 0, $t['base']['suffix'] ?? ''); $pmAvgIncome = $fmt($t['base']['prefix'] ?? '', $avg['prev_month']['income'] ?? 0, $t['base']['suffix'] ?? ''); $pmAvgExpenses = $fmt($t['base']['prefix'] ?? '', $avg['prev_month']['expenses'] ?? 0, $t['base']['suffix'] ?? ''); $pmAvgProfit = $fmt($t['base']['prefix'] ?? '', $avg['prev_month']['profit'] ?? 0, $t['base']['suffix'] ?? ''); $tyAvgIncome = $fmt($t['base']['prefix'] ?? '', $avg['tax_year']['income'] ?? 0, $t['base']['suffix'] ?? ''); $tyAvgExpenses = $fmt($t['base']['prefix'] ?? '', $avg['tax_year']['expenses'] ?? 0, $t['base']['suffix'] ?? ''); $tyAvgProfit = $fmt($t['base']['prefix'] ?? '', $avg['tax_year']['profit'] ?? 0, $t['base']['suffix'] ?? ''); $lastTyAvgIncome = $fmt($t['base']['prefix'] ?? '', $avg['last_tax_year']['income'] ?? 0, $t['base']['suffix'] ?? ''); $lastTyAvgExpenses = $fmt($t['base']['prefix'] ?? '', $avg['last_tax_year']['expenses'] ?? 0, $t['base']['suffix'] ?? ''); $lastTyAvgProfit = $fmt($t['base']['prefix'] ?? '', $avg['last_tax_year']['profit'] ?? 0, $t['base']['suffix'] ?? ''); $tyMonths = (int)($avg['tax_year']['months'] ?? 0); $isInvoice = ($m['mode'] === 'invoice'); $modeLabel = $isInvoice ? 'Invoice-linked' : 'All transactions'; $switchTo = $isInvoice ? 'all' : 'invoice'; $switchTxt = $isInvoice ? 'Switch to All transactions' : 'Switch to Invoice-linked'; $switchUrl = $this->toggleUrl($switchTo); return <<<HTML <div class="widget-content-padded"> <div class="clearfix"> <div class="pull-left"> <strong>Totals in Base ({$t['base']['code']})</strong> <span class="label label-info" style="margin-left:8px;">Mode: {$modeLabel}</span> <a href="{$switchUrl}" style="margin-left:8px;">{$switchTxt}</a> </div> <div class="pull-right text-muted" style="font-size:12px"> Generated {$m['generated']} </div> </div> <table class="table table-condensed" style="margin-top:10px"> <thead> <tr> <th></th> <th class="text-right">This Month<br><small>({$m['month_label']})</small></th> <th class="text-right">Previous Month<br><small>({$m['prev_month_label']})</small></th> <th class="text-right"> Current Tax Year (to date) <br><small>({$m['tax_year_label']}: {$m['tax_year_full_range']})</small> <br><small class="text-muted">As of {$m['tax_year_asof']}</small> </th> <th class="text-right"> Last Tax Year (full) <br><small>({$m['last_tax_year_label']}: {$m['last_tax_year_full_range']})</small> </th> </tr> </thead> <tbody> <tr> <th>Total (Base {$t['base']['code']})</th> <td class="text-right">{$monthBase}</td> <td class="text-right">{$prevMonthBase}</td> <td class="text-right">{$tyBase}</td> <td class="text-right">{$lastTyBase}</td> </tr> <tr> <th> Average per month (Income, Expenses, Profit) <br><small class="text-muted">Current tax year: {$tyMonths}/12 tax months elapsed; last tax year: 12/12</small> </th> <td class="text-right"> <div><small class="text-muted">Income</small> {$mAvgIncome}</div> <div><small class="text-muted">Expenses</small> {$mAvgExpenses}</div> <div><small class="text-muted">Profit</small> {$mAvgProfit}</div> </td> <td class="text-right"> <div><small class="text-muted">Income</small> {$pmAvgIncome}</div> <div><small class="text-muted">Expenses</small> {$pmAvgExpenses}</div> <div><small class="text-muted">Profit</small> {$pmAvgProfit}</div> </td> <td class="text-right"> <div><small class="text-muted">Income</small> {$tyAvgIncome}</div> <div><small class="text-muted">Expenses</small> {$tyAvgExpenses}</div> <div><small class="text-muted">Profit</small> {$tyAvgProfit}</div> </td> <td class="text-right"> <div><small class="text-muted">Income</small> {$lastTyAvgIncome}</div> <div><small class="text-muted">Expenses</small> {$lastTyAvgExpenses}</div> <div><small class="text-muted">Profit</small> {$lastTyAvgProfit}</div> </td> </tr> </tbody> </table> <p class="text-muted" style="font-size:12px;margin-top:8px;"> Formula: <code>Amount In − Fees − Amount Out</code>. UK tax year runs 6 Apr – 5 Apr. <br>Average per month uses: <code>Income = Amount In</code>, <code>Expenses = Fees + Amount Out</code>, <code>Profit = Amount In − Fees − Amount Out</code>. <br><small>Note: “This Month” is month-to-date, so it updates daily.</small> </p> </div> HTML; } }; }); This code and guidance are provided as-is for general informational purposes only. I’m not an accountant, tax adviser, or solicitor, and this widget should not be relied on as professional financial, accounting, or tax advice. Use at your own risk. No liability is accepted for any loss, damage, or issues arising from the use of this code.
  6. Good day, In the WHMCS client area, invoices are currently listed with the oldest invoice at the top and the newest at the bottom. How can I change this so that the most recent invoice appears at the top of the list? Thanks
  7. Hi @kon, I'm pleased to hear you like the direction we're going with Nexus cart! We're going to continue with this work in 9.1 and bring this same design language to the rest of the purchase flow. The RDAP implementation has been rescheduled to v9.2. This is because we have prioritised security and financial compliance features in releasing 9.0 and will continue this focus in the subsequent 9.1 (eg. credit notes, e-invoicing, improved VAT compliance).
  8. Can you tell me using which domain registrar module? Also please message me the out put of the below commands from your terminal. 1) openssl version 2) php -i | grep -i openssl this will help to understand the openssl versions used by php and terminal, to know there is any issue to openssl versions.
  9. Last week
  10. i cant connect to my domain registrar through their module. showing handshake error. but when i try to check it through terminal manually its connecting. so how to check it and fix it. i checked it with multiple registrars like namesilo, resellerclub and connectreseller
  11. Website: https://www.virtualskyhost.com Billing & Automation Platform: WHMCS Hello WHMCS Community, I’d like to showcase Virtual Sky Host, a hosting provider offering a range of fully automated hosting solutions built around WHMCS. Our current services include: Web Hosting WordPress Hosting WooCommerce Hosting Reseller Hosting Automated VPS Hosting WHMCS is used for client onboarding, automated provisioning, billing, invoicing, and service management across all of our offerings. Our focus is on providing a simple, reliable, and transparent hosting experience, with room to scale as customer needs grow. We’re continuously refining our WHMCS setup and backend infrastructure as the platform evolves. Feedback and suggestions from the WHMCS community are always welcome. Thanks for taking the time to check out our site.
  12. As long as that meets data privacy requirements for wherever you are located, I'm happy for you. Even more so if you fully trust the platform. Do what's right for you and your clients.
  13. This is something I could do, but I have never done this in the past and the numbering worked fine. I do not understand why it would have changed. I use to just generate a report and the invoice displayed the correct number, but they are blank if I do not add the number in the 'options' section.
  14. I am simply expressing my feedback. The price rise is unjust, I feel. Now, in the Australian dollar it is now $52 /mo What exactly has been done to the software that justifies the price increase?
  15. Appreciate the reply - but my (ChatGPT) hook is doing what it needs to for me i.e stripping `&prompt=consent` 🙂
  16. Data export, deletion requests, consent logging, cookie banner, DPA management, auto-anonymization. Features Data export - Clients export their data (CSV/JSON). Admin can export any client. Deletion requests - Client requests, you approve, system anonymizes. Invoices kept for legal. Consent logging - IP/user agent tracked. Auto-log on registration optional. Cookie banner - Styling, position, animations. Cookie policy page included. DPA management - Track subprocessors. Client DPA acceptance with version control. Auto-anonymization - Closed inactive accounts anonymized after X years. Warning email first. Audit trail - Everything logged. Not included Legal advice Your privacy policy Compliance guarantees Compatibility WHMCS 8.x, 9.x. Templates: Six, Twenty-One, Lagom, Starter. Client DPA Enable, set version, add PDF URLs per language. Clients see banner until accepted. Change version = re-acceptance required. 26 languages. Auto-Anonymization Finds closed accounts with no login, no services, no activity for X years. Warning email. No login during warning period = anonymized. Exclude specific clients if needed. Anonymization Personal data hashed. Tickets redacted. IPs cleared. Account closed. Invoices kept (7 years Belgian law, check yours). Languages EN, NL, DE, FR, IT, ES, RU. Links Marketplace: marketplace.whmcs.com/product/8376-gdpr-suite (might still be under review) Order: arkhost.com/store/whmcs-modules/gdpr-suite Questions here or support@arkhost.com.
  17. Hello guys, First of all thank you brian for great idea and i wish you are still around here. I like this whole idea and i was using it for some time, but it was missing some features and links were not working properly based on links setup. So i went forward and extended this idea into WHMCS addon module with extended features. https://builtbybit.com/resources/admin-info-bar-whmcs-module.88178/ ## Key Benefits Elevates the admin experience with a clean, informative stats bar Speeds up operations via live configuration and instant visual feedback Reduces theme inconsistencies with Bootstrap-friendly components Works across all admin templates and Friendly URLs modes ## Core Features - Customizable appearance - Color pickers for bar background, text, and count highlight - Font family selector (system, serif, sans, mono, inherit...) - Font size presets (em/px) - Live preview updates as you change settings - Rich metrics (toggle on/off per metric) - Pending Orders - Overdue Invoices - Tickets Awaiting Reply - Todo Items Due - Active Clients - Active Services - Pending Cancellations - Expired Domains - Current Date & Time - Icon customization - Set per-metric icons (Font Awesome class names) - Consistent spacing and visibility within the bar - Bootstrap-aligned admin UI - Tabs, panels, forms, alerts, and buttons match WHMCS styling - Responsive layout that adapts to mobile and wide screens - Cross-template compatibility - No restriction to a single admin theme; runs on blend, lara, and customs - Friendly URLs awareness - Detects Full Friendly Rewrite vs index.php routes automatically - Generates correct admin links for both modes ## Smart persistence - One-click Save for all settings - Live preview updates without saving until you click Save - Safe and consistent - Sanitized hex colors with validation - Whitelisted font families and sizes
  18. Hi @SVCode I received a follow-up from WHMCS Support regarding this issue. They have confirmed that the `prompt=consent` parameter is the culprit preventing connections. They provided me with a modified `MicrosoftAuthProvider.php` file that removes this parameter, and I can confirm this fixes the issue for us permanently on WHMCS 8.13.1. Just to clarify on the workaround mentioned earlier: we did not copy the URL to a new tab (which likely breaks the session/state); we simply edited the URL directly in the current browser tab to remove `&prompt=consent` and hit enter. However, for a permanent fix without manual URL editing, you will need the patched file. Since the file is IonCube encoded, you cannot easily edit it yourself. The Solution:
I recommend opening a ticket with WHMCS support and referencing case WHMCS-24661. They should be able to provide you with the same 'MicrosoftAuthProvider.php' patch for version 8.13.1 that they sent me. Once you have the file: 1. Backup your original /vendor/whmcs/whmcs-foundation/lib/Mail/Incoming/Provider/MicrosoftAuthProvider.php 2. Upload the patched file to the same location. 3. Re-authenticate your department using the Microsoft 365 licensed user. Hope this helps you get yours connected! MicrosoftAuthProvider.php
  19. ChatGPT to the rescue - I had to use a hook to strip it out, all browsers I tried blocked editing the address bar (even after trying to avoid it with browser settings): <?php /** * Strip prompt=consent from Microsoft OAuth authorize URLs opened by WHMCS popups. * Upgrade-safe: lives in /includes/hooks */ add_hook('AdminAreaHeadOutput', 1, function ($vars) { return <<<HTML <script> (function () { function stripPromptConsent(url) { try { var u = new URL(url, window.location.href); // Only touch Microsoft login/authorize hosts var host = (u.hostname || "").toLowerCase(); var isMicrosoftLogin = host === "login.microsoftonline.com" || host.endsWith(".login.microsoftonline.com") || host.endsWith(".microsoftonline.com"); if (!isMicrosoftLogin) return url; var prompt = u.searchParams.get("prompt"); if (prompt && prompt.toLowerCase() === "consent") { u.searchParams.delete("prompt"); return u.toString(); } return url; } catch (e) { return url; // if URL parsing fails, do nothing } } // Patch window.open (most popup flows use this) var _open = window.open; window.open = function (url, name, specs, replace) { if (typeof url === "string") { url = stripPromptConsent(url); } return _open.call(this, url, name, specs, replace); }; // Also patch location.assign/replace in case WHMCS uses those try { var _assign = window.location.assign.bind(window.location); window.location.assign = function (url) { if (typeof url === "string") url = stripPromptConsent(url); return _assign(url); }; } catch (e) {} try { var _replace = window.location.replace.bind(window.location); window.location.replace = function (url) { if (typeof url === "string") url = stripPromptConsent(url); return _replace(url); }; } catch (e) {} })(); </script> HTML; });
  20. @BENELUX How do you strip the `&prompt=consent` in the URL? The address box isn't editable in the pop up - tried a few browsers - and if I copy/paste it into a new window, it doesn't seem to update the WHMCS window with the token.
  21. 🚀 Introducing Hostina — The Best WHMCS Hosting Template (LTR & RTL) Great news! After months of professional design and development, we’re proud to announce the public release of Hostina — a premium WHMCS hosting template built for modern hosting businesses. Hostina is a fully responsive, bilingual (English LTR + Arabic RTL) WHMCS theme, designed specifically for web hosting companies, domain registrars, resellers, and cloud providers who want a clean, fast, and high-converting client experience. Combining a modern interface, a custom WHMCS dashboard, and a fully styled orderform checkout, Hostina delivers a smooth and professional user experience across all devices. ✨ Key Features - Premium modern & responsive WHMCS design - Full Arabic (RTL) + English (LTR) bilingual support - SEO optimized structure (Core Web Vitals ready) - Custom WHMCS client dashboard with sidebar navigation - Styled orderform & checkout pages - Easy installation — upload, activate, and launch in minutes 🧭 Frontend Pages Included - Homepage (features, pricing, testimonials, FAQ) - Domain Search & Domain Transfer - Hosting Plans Pages: Shared Hosting, WordPress Hosting, Reseller Hosting, VPS, Dedicated Servers, Game Servers - Cart & Checkout (Styled WHMCS Orderform Flow) - Blog & Blog Details - Contact Page (form + map) 💼 WHMCS Client Area - Login & Registration - Custom Dashboard with Sidebar - My Services & Products - Domain Management (renewals, auto-renew, DNS) - Invoices & Billing - Support Tickets - User & Membership Management ⚙️ Compatibility - WHMCS: 8.0 – 8.13+ - PHP: 8.1 – 8.3 - Compatible with cPanel, Plesk, DirectAdmin, CloudLinux, LiteSpeed, Apache, Nginx 🖼️ Screenshots Preview https://hostk.com/hostina-template 🧪 Live Demo Frontend Demo: https://demo.hostk.com/hostina/ English Version: https://demo.hostk.com/hostina/en Arabic Version (RTL): https://demo.hostk.com/hostina/ar WHMCS Client Area Demo: https://demo.hostk.com/index.php?systpl=hostina Login: test@hostk.com Password: test 🛒 Purchase Hostina WHMCS Template Hostina is a complete WHMCS solution — modern template, custom dashboard, and styled checkout. If you’re looking for the best WHMCS hosting template with Arabic & English support, Hostina is built for you.
  22. This is the top 10 WHMCS Services modules of the year 2025 01) Email 2FA 02) SMS Manager 03) Email Verification Pro 04) Services Fee 05) Support Pin 06) Affiliates Plus 07) Agree Terms 08) Client Manager 09) Discount Manager 10) Refer A Friend
  23. Hello, A couple of notes for feedback for @WHMCS John: The Nexus Cart is amazing! It's what we've been asking for in the recent years. New and fresh look that users will like to interact with. Thank you! For the Expanded API Coverage, please kindly include more examples in the documentation. If we have examples (even simple ones) on how we can interact with the API on each occasion, it will be hands down a much more easier use of it for us, Now, for the main subject of this thread. We're following the feature requests a lot. One of the expected additions for us on the newest release was RDAP and the feature request is here: https://requests.whmcs.com/idea/rdap-whois-query-integration Didn't see anything in the Release Notes of 8.13 and 9.0 and this is why i ask. The comment in the feature request was saying "RDAP support is currently planned for the second release of 2025 (after 8.13). " Please correct me if we've missed it. Could you please let us know if it will be part of v9.0?
  24. Hi @BENELUX, 9.0 stable release will be made in January 2026. The exact date will depend on the feedback we get and progress addressing cases.
  25. I asked via ticket and was told early January. So I would assume that means between now and the 10th.
  26. Hello. I have the same problem on my whmcs 8.13.1. Client orderd server upgrade. Payments done for upgrade. The module updated to a different tariff, but the letter to the customer did not arrive.
  27. Could you please let us know if there is an estimated timeline for the WHMCS 9.0 stable release?
  1. Load more activity
×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use & Guidelines and understand your posts will initially be pre-moderated