diff --git a/.idea/SonarDesk.iml b/.idea/SonarDesk.iml
index 2be73f3..0fd1146 100644
--- a/.idea/SonarDesk.iml
+++ b/.idea/SonarDesk.iml
@@ -5,6 +5,9 @@
+
+
+
diff --git a/sonar-desk-licensing/app/Http/Controllers/Admin/ReleaseController.php b/sonar-desk-licensing/app/Http/Controllers/Admin/ReleaseController.php
new file mode 100644
index 0000000..2d2b5e2
--- /dev/null
+++ b/sonar-desk-licensing/app/Http/Controllers/Admin/ReleaseController.php
@@ -0,0 +1,101 @@
+ SonarDeskRelease::latest()->paginate(30),
+ 'publishedCount' => SonarDeskRelease::where('is_published', true)->count(),
+ 'downloadCount' => SonarDeskRelease::sum('download_count'),
+ ]);
+ }
+
+ public function create() { return view('admin.releases.form', ['release' => new SonarDeskRelease]); }
+ public function edit(SonarDeskRelease $release) { return view('admin.releases.form', compact('release')); }
+
+ public function store(Request $request): RedirectResponse
+ {
+ $release = new SonarDeskRelease;
+ $this->persist($request, $release);
+ return redirect()->route('admin.releases.index')->with('status', 'Release created.');
+ }
+
+ public function update(Request $request, SonarDeskRelease $release): RedirectResponse
+ {
+ $this->persist($request, $release);
+ return redirect()->route('admin.releases.index')->with('status', 'Release updated.');
+ }
+
+ public function destroy(SonarDeskRelease $release): RedirectResponse
+ {
+ if ($release->file_path) Storage::disk('local')->delete($release->file_path);
+ $release->delete();
+ return back()->with('status', 'Release deleted.');
+ }
+
+ public function settings()
+ {
+ $keys = ['downloads.eyebrow','downloads.title','downloads.intro','downloads.requirements','downloads.support_text'];
+ $settings = collect($keys)->mapWithKeys(fn ($key) => [$key => SiteSetting::valueFor($key)]);
+ return view('admin.releases.settings', compact('settings'));
+ }
+
+ public function updateSettings(Request $request): RedirectResponse
+ {
+ $data = $request->validate([
+ 'eyebrow' => ['required','string','max:80'], 'title' => ['required','string','max:120'],
+ 'intro' => ['required','string','max:500'], 'requirements' => ['required','string','max:500'],
+ 'support_text' => ['required','string','max:500'],
+ ]);
+ foreach ($data as $key => $value) SiteSetting::put('downloads.'.$key, $value);
+ return back()->with('status', 'Download page content updated.');
+ }
+
+ private function persist(Request $request, SonarDeskRelease $release): void
+ {
+ $data = $request->validate([
+ 'version' => ['required','string','max:40'], 'build_number' => ['required','integer','min:1'],
+ 'channel' => ['required', Rule::in(['stable','beta'])], 'minimum_macos' => ['nullable','string','max:30'],
+ 'release_notes' => ['nullable','string','max:20000'], 'sparkle_signature' => ['nullable','string','max:2000'],
+ 'package' => [
+ $release->exists ? 'nullable' : 'required',
+ 'file',
+ 'max:1048576',
+ function (string $attribute, $value, \Closure $fail): void {
+ $extension = strtolower($value->getClientOriginalExtension());
+ if (! in_array($extension, ['dmg', 'zip'], true)) {
+ $fail('The package must be a DMG or ZIP file.');
+ }
+ },
+ ],
+ 'is_published' => ['nullable','boolean'], 'is_featured' => ['nullable','boolean'],
+ ]);
+ $data['is_published'] = $request->boolean('is_published');
+ $data['is_featured'] = $request->boolean('is_featured');
+ if ($data['is_featured']) SonarDeskRelease::query()->update(['is_featured' => false]);
+
+ if ($request->hasFile('package')) {
+ if ($release->file_path) Storage::disk('local')->delete($release->file_path);
+ $file = $request->file('package');
+ $path = $file->store('sonardesk/releases', 'local');
+ $data['file_path'] = $path;
+ $data['original_filename'] = $file->getClientOriginalName();
+ $data['file_size'] = $file->getSize();
+ $data['sha256'] = hash_file('sha256', $file->getRealPath());
+ }
+ unset($data['package']);
+ $data['published_at'] = $data['is_published'] ? ($release->published_at ?: now()) : null;
+ $release->fill($data)->save();
+ }
+}
diff --git a/sonar-desk-licensing/app/Http/Controllers/CheckoutController.php b/sonar-desk-licensing/app/Http/Controllers/CheckoutController.php
index 47a5f82..9c8d5c5 100755
--- a/sonar-desk-licensing/app/Http/Controllers/CheckoutController.php
+++ b/sonar-desk-licensing/app/Http/Controllers/CheckoutController.php
@@ -17,10 +17,10 @@ final class CheckoutController extends Controller
}
$plans = [
- 'personal-monthly' => ['name' => 'Personal', 'billing' => 'Monthly', 'display_price' => '£2.99 / month', 'price_id' => $paddle['personal_monthly_price_id'], 'activation_limit' => 1],
- 'personal-annual' => ['name' => 'Personal', 'billing' => 'Annual', 'display_price' => '£24.99 / year', 'price_id' => $paddle['personal_annual_price_id'], 'activation_limit' => 1],
- 'professional-monthly' => ['name' => 'Professional', 'billing' => 'Monthly', 'display_price' => '£4.99 / month', 'price_id' => $paddle['professional_monthly_price_id'], 'activation_limit' => 3],
- 'professional-annual' => ['name' => 'Professional', 'billing' => 'Annual', 'display_price' => '£44.99 / year', 'price_id' => $paddle['professional_annual_price_id'], 'activation_limit' => 3],
+ 'personal-monthly' => ['name' => 'Personal', 'billing' => 'Monthly', 'display_price' => '£4.99 / month', 'price_id' => $paddle['personal_monthly_price_id'], 'activation_limit' => 1],
+ 'personal-annual' => ['name' => 'Personal', 'billing' => 'Annual', 'display_price' => '£39.99 / year', 'price_id' => $paddle['personal_annual_price_id'], 'activation_limit' => 1],
+ 'professional-monthly' => ['name' => 'Professional', 'billing' => 'Monthly', 'display_price' => '£8.99 / month', 'price_id' => $paddle['professional_monthly_price_id'], 'activation_limit' => 3],
+ 'professional-annual' => ['name' => 'Professional', 'billing' => 'Annual', 'display_price' => '£79.99 / year', 'price_id' => $paddle['professional_annual_price_id'], 'activation_limit' => 3],
];
foreach ($plans as $plan) {
diff --git a/sonar-desk-licensing/app/Http/Controllers/ReleaseController.php b/sonar-desk-licensing/app/Http/Controllers/ReleaseController.php
new file mode 100644
index 0000000..f8246af
--- /dev/null
+++ b/sonar-desk-licensing/app/Http/Controllers/ReleaseController.php
@@ -0,0 +1,58 @@
+where('is_featured', true)->orderByDesc('published_at')->first()
+ ?? SonarDeskRelease::published()->where('channel', 'stable')->orderByDesc('published_at')->orderByDesc('id')->first();
+ $releases = SonarDeskRelease::published()->orderByDesc('published_at')->paginate(12);
+ $settings = $this->settings();
+
+ return view('downloads.index', compact('latest', 'releases', 'settings'));
+ }
+
+ public function latest(): BinaryFileResponse
+ {
+ $release = SonarDeskRelease::published()->where('channel', 'stable')->orderByDesc('published_at')->firstOrFail();
+ return $this->send($release);
+ }
+
+ public function download(SonarDeskRelease $release): BinaryFileResponse
+ {
+ abort_unless($release->is_published && $release->published_at && $release->file_path, 404);
+ return $this->send($release);
+ }
+
+ public function appcast(): Response
+ {
+ $releases = SonarDeskRelease::published()->where('channel', 'stable')->orderByDesc('published_at')->limit(20)->get();
+ return response()->view('downloads.appcast', compact('releases'), 200, ['Content-Type' => 'application/rss+xml; charset=UTF-8']);
+ }
+
+ private function send(SonarDeskRelease $release): BinaryFileResponse
+ {
+ abort_unless(Storage::disk('local')->exists($release->file_path), 404);
+ $release->increment('download_count');
+ return response()->download(Storage::disk('local')->path($release->file_path), $release->original_filename ?: 'SonarDesk-'.$release->version.'.dmg');
+ }
+
+ private function settings(): array
+ {
+ return [
+ 'eyebrow' => SiteSetting::valueFor('downloads.eyebrow', 'SonarDesk for macOS'),
+ 'title' => SiteSetting::valueFor('downloads.title', 'Download SonarDesk'),
+ 'intro' => SiteSetting::valueFor('downloads.intro', 'A focused native SonarQube workspace, designed for macOS and built for daily engineering work.'),
+ 'requirements' => SiteSetting::valueFor('downloads.requirements', 'Apple silicon and Intel Macs supported. A SonarQube server and valid SonarDesk licence are required.'),
+ 'support_text' => SiteSetting::valueFor('downloads.support_text', 'Need help installing or activating SonarDesk? Our support team can help.'),
+ ];
+ }
+}
diff --git a/sonar-desk-licensing/app/Models/SiteSetting.php b/sonar-desk-licensing/app/Models/SiteSetting.php
new file mode 100644
index 0000000..83af974
--- /dev/null
+++ b/sonar-desk-licensing/app/Models/SiteSetting.php
@@ -0,0 +1,26 @@
+ static::query()->find($key)?->value ?? $default);
+ }
+
+ public static function put(string $key, ?string $value): void
+ {
+ static::query()->updateOrCreate(['key' => $key], ['value' => $value]);
+ Cache::forget('sonardesk.setting.'.$key);
+ }
+}
diff --git a/sonar-desk-licensing/app/Models/SonarDeskRelease.php b/sonar-desk-licensing/app/Models/SonarDeskRelease.php
new file mode 100644
index 0000000..df4dbb2
--- /dev/null
+++ b/sonar-desk-licensing/app/Models/SonarDeskRelease.php
@@ -0,0 +1,40 @@
+ 'boolean',
+ 'is_featured' => 'boolean',
+ 'published_at' => 'datetime',
+ 'file_size' => 'integer',
+ 'download_count' => 'integer',
+ ];
+ }
+
+ public function scopePublished($query)
+ {
+ return $query->where('is_published', true)->whereNotNull('file_path')->whereNotNull('published_at');
+ }
+
+ public function getFormattedSizeAttribute(): string
+ {
+ $bytes = (int) $this->file_size;
+ if ($bytes < 1024) return $bytes.' B';
+ if ($bytes < 1048576) return number_format($bytes / 1024, 1).' KB';
+ if ($bytes < 1073741824) return number_format($bytes / 1048576, 1).' MB';
+ return number_format($bytes / 1073741824, 2).' GB';
+ }
+}
diff --git a/sonar-desk-licensing/app/Services/Licensing/LicenseDeliveryService.php b/sonar-desk-licensing/app/Services/Licensing/LicenseDeliveryService.php
index 38a2c05..0c1e584 100755
--- a/sonar-desk-licensing/app/Services/Licensing/LicenseDeliveryService.php
+++ b/sonar-desk-licensing/app/Services/Licensing/LicenseDeliveryService.php
@@ -38,6 +38,7 @@ final class LicenseDeliveryService
'licenseKey' => $plain,
'expiresAt' => $license->expires_at,
'portalUrl' => config('sonardesk.portal_url') ?: url('/portal'),
+ 'downloadUrl' => route('release'),
];
Mail::send(['html' => 'emails.license-key', 'text' => 'emails.license-key-text'], $data, function ($message) use ($recipient): void {
diff --git a/sonar-desk-licensing/app/Services/Licensing/LicenseTokenVerifier.php b/sonar-desk-licensing/app/Services/Licensing/LicenseTokenVerifier.php
index fa497fc..a13ba47 100755
--- a/sonar-desk-licensing/app/Services/Licensing/LicenseTokenVerifier.php
+++ b/sonar-desk-licensing/app/Services/Licensing/LicenseTokenVerifier.php
@@ -14,10 +14,11 @@ final class LicenseTokenVerifier
}
$payload = $this->decode($parts[1]);
$signature = $this->decode($parts[2]);
- $public = base64_decode((string) config('sonardesk.public_key'), true);
- if ($public === false || strlen($public) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) {
- throw new RuntimeException('Server licence public key is invalid.');
- }
+ // Derive the verification key from the same private seed used by LicenseSigner.
+ // Keeping a separate SONARDESK_LICENSE_PUBLIC_KEY allowed the two keys to drift:
+ // activation would sign a perfectly valid token with the new private key, then
+ // /validate would reject that same token using a stale public key.
+ $public = $this->publicKeyFromPrivateSeed();
if (! sodium_crypto_sign_verify_detached($signature, $payload, $public)) {
throw new RuntimeException('Licence token signature is invalid.');
}
@@ -26,6 +27,23 @@ final class LicenseTokenVerifier
return is_array($decoded) ? $decoded : throw new RuntimeException('Licence token payload is invalid.');
}
+ private function publicKeyFromPrivateSeed(): string
+ {
+ $path = config('sonardesk.private_key_path');
+ if (! $path || ! is_file($path)) {
+ throw new RuntimeException('SONARDESK_LICENSE_PRIVATE_KEY_PATH is not configured or does not exist.');
+ }
+
+ $seed = base64_decode(trim((string) file_get_contents($path)), true);
+ if ($seed === false || strlen($seed) !== SODIUM_CRYPTO_SIGN_SEEDBYTES) {
+ throw new RuntimeException('The SonarDesk private key must be a Base64-encoded 32-byte Ed25519 seed.');
+ }
+
+ $pair = sodium_crypto_sign_seed_keypair($seed);
+
+ return sodium_crypto_sign_publickey($pair);
+ }
+
private function decode(string $value): string
{
$value = strtr($value, '-_', '+/');
diff --git a/sonar-desk-licensing/database/migrations/2026_09_30_150000_create_sonardesk_releases_table.php b/sonar-desk-licensing/database/migrations/2026_09_30_150000_create_sonardesk_releases_table.php
new file mode 100644
index 0000000..e82cce2
--- /dev/null
+++ b/sonar-desk-licensing/database/migrations/2026_09_30_150000_create_sonardesk_releases_table.php
@@ -0,0 +1,42 @@
+id();
+ $table->string('version', 40);
+ $table->unsignedInteger('build_number')->default(1);
+ $table->string('channel', 20)->default('stable');
+ $table->string('minimum_macos', 30)->nullable();
+ $table->string('file_path')->nullable();
+ $table->string('original_filename')->nullable();
+ $table->unsignedBigInteger('file_size')->nullable();
+ $table->string('sha256', 64)->nullable();
+ $table->text('sparkle_signature')->nullable();
+ $table->text('release_notes')->nullable();
+ $table->boolean('is_published')->default(false)->index();
+ $table->boolean('is_featured')->default(false)->index();
+ $table->timestamp('published_at')->nullable()->index();
+ $table->unsignedBigInteger('download_count')->default(0);
+ $table->timestamps();
+ $table->unique(['version', 'build_number', 'channel']);
+ });
+
+ Schema::create('sonardesk_site_settings', function (Blueprint $table): void {
+ $table->string('key')->primary();
+ $table->text('value')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('sonardesk_site_settings');
+ Schema::dropIfExists('sonardesk_releases');
+ }
+};
diff --git a/sonar-desk-licensing/deploy.php b/sonar-desk-licensing/deploy.php
index 628690d..634da4c 100755
--- a/sonar-desk-licensing/deploy.php
+++ b/sonar-desk-licensing/deploy.php
@@ -18,11 +18,11 @@ add('shared_dirs', ['storage']);
add('writable_dirs', ['storage', 'bootstrap/cache']);
// Host Definitions
-host('production')
- ->set('hostname', 'sonardsk.absdev.net')
+host('main')
+ ->set('hostname', 'sonardesk.absdev.net')
->set('remote_user', 'mag')
->set('branch', 'main')
- ->set('deploy_path', '/var/www/sonardesk/main');
+ ->set('deploy_path', '/var/www/sonardesk/prod');
host('stage')
->set('hostname', 'sonardesk.stage.absdev.net')
diff --git a/sonar-desk-licensing/AGENTS.md b/sonar-desk-licensing/docs/AGENTS.md
similarity index 100%
rename from sonar-desk-licensing/AGENTS.md
rename to sonar-desk-licensing/docs/AGENTS.md
diff --git a/sonar-desk-licensing/CLAUDE.md b/sonar-desk-licensing/docs/CLAUDE.md
similarity index 100%
rename from sonar-desk-licensing/CLAUDE.md
rename to sonar-desk-licensing/docs/CLAUDE.md
diff --git a/sonar-desk-licensing/MARKETING_LAUNCH_GUIDE.md b/sonar-desk-licensing/docs/MARKETING_LAUNCH_GUIDE.md
similarity index 99%
rename from sonar-desk-licensing/MARKETING_LAUNCH_GUIDE.md
rename to sonar-desk-licensing/docs/MARKETING_LAUNCH_GUIDE.md
index a948ee0..3b43fd8 100644
--- a/sonar-desk-licensing/MARKETING_LAUNCH_GUIDE.md
+++ b/sonar-desk-licensing/docs/MARKETING_LAUNCH_GUIDE.md
@@ -107,7 +107,7 @@ The current code uses subscription events as the authoritative state for recurri
Copy the webhook endpoint secret.
-### E. Production `.env`
+### E. Production `../.env`
Set the following values on the production server. Do not commit them.
@@ -135,7 +135,7 @@ Also configure the existing SonarDesk licence signing keys, mailer, support emai
### F. Seed the price mapping
-After the production IDs are in `.env`:
+After the production IDs are in `../.env`:
```bash
php artisan config:clear
diff --git a/sonar-desk-licensing/docs/RELEASE_DISTRIBUTION.md b/sonar-desk-licensing/docs/RELEASE_DISTRIBUTION.md
new file mode 100644
index 0000000..536ab03
--- /dev/null
+++ b/sonar-desk-licensing/docs/RELEASE_DISTRIBUTION.md
@@ -0,0 +1,28 @@
+# SonarDesk release distribution
+
+The website now owns SonarDesk release distribution from the admin area.
+
+## Deploy
+
+1. Deploy the code normally.
+2. Run `php artisan migrate --force`.
+3. Ensure Laravel's `storage` directory remains shared/persistent between releases (the existing Deployer configuration already shares `storage`).
+4. Ensure the web/PHP upload limits are large enough for the DMG. For a 1 GB application limit, configure nginx `client_max_body_size` and PHP `upload_max_filesize` / `post_max_size` accordingly.
+
+## Publishing a build
+
+Open **Admin → Releases → New release**. Enter the marketing version, build number, minimum macOS version and release notes, upload the signed/notarized DMG or ZIP, and paste the Sparkle EdDSA signature when Sparkle is enabled. Save as a draft until the package is ready, then publish it.
+
+Published stable releases automatically feed:
+
+- `/release` — customer-facing distribution centre
+- `/download` — latest stable package
+- `/updates/appcast.xml` — Sparkle feed
+- the licence portal download call-to-action
+- licence delivery email download call-to-action
+
+The SHA-256 checksum and file size are calculated on upload. Download counts are tracked per release.
+
+## Sparkle
+
+Generate the EdDSA signature with the Sparkle release tooling and paste it into the release editor. Configure the macOS app's feed URL to the production `/updates/appcast.xml` endpoint. Do not place Sparkle private signing keys on the public web server.
diff --git a/sonar-desk-licensing/false b/sonar-desk-licensing/false
new file mode 100644
index 0000000..e69de29
diff --git a/sonar-desk-licensing/resources/views/admin/index.blade.php b/sonar-desk-licensing/resources/views/admin/index.blade.php
index 85171f0..f4198c6 100755
--- a/sonar-desk-licensing/resources/views/admin/index.blade.php
+++ b/sonar-desk-licensing/resources/views/admin/index.blade.php
@@ -2,5 +2,5 @@
@section('content')
{{ $licenseCount }} Licences
{{ $activeCount }} Not revoked
{{ $activationCount }} Active installations
{{ $userCount }} Portal users
-
+
@endsection
diff --git a/sonar-desk-licensing/resources/views/checkout/buy.blade.php b/sonar-desk-licensing/resources/views/checkout/buy.blade.php
index 76ff5be..cb36c03 100755
--- a/sonar-desk-licensing/resources/views/checkout/buy.blade.php
+++ b/sonar-desk-licensing/resources/views/checkout/buy.blade.php
@@ -61,7 +61,7 @@
Paddle.PricePreview({items: previewItems}).then(result => {
for (const item of (result?.data?.details?.lineItems ?? [])) {
const element = priceElements.get(item?.price?.id);
- const formatted = item?.formattedTotals?.subtotal ?? item?.formattedUnitTotals?.subtotal;
+ const formatted = item?.formattedTotals?.total ?? item?.formattedUnitTotals?.total;
if (!element || !formatted) continue;
const suffix = element.dataset.billing === 'annual' ? ' / year' : ' / month';
element.textContent = formatted + suffix;
diff --git a/sonar-desk-licensing/resources/views/downloads/appcast.blade.php b/sonar-desk-licensing/resources/views/downloads/appcast.blade.php
new file mode 100644
index 0000000..ca21258
--- /dev/null
+++ b/sonar-desk-licensing/resources/views/downloads/appcast.blade.php
@@ -0,0 +1,2 @@
+{!! '' !!}
+SonarDesk Updates {{ route('updates.appcast') }}SonarDesk stable release feed @foreach($releases as $release)SonarDesk {{ $release->version }} {{ $release->published_at?->toRfc2822String() }} {{ $release->build_number }} {{ $release->version }} @if($release->minimum_macos){{ $release->minimum_macos }} @endifrelease_notes ?: 'SonarDesk update')) !!}]]> sparkle_signature) sparkle:edSignature="{{ $release->sparkle_signature }}"@endif /> @endforeach
diff --git a/sonar-desk-licensing/resources/views/downloads/index.blade.php b/sonar-desk-licensing/resources/views/downloads/index.blade.php
new file mode 100644
index 0000000..0d66597
--- /dev/null
+++ b/sonar-desk-licensing/resources/views/downloads/index.blade.php
@@ -0,0 +1,8 @@
+@extends('layouts.app', ['title' => 'Download SonarDesk — Native SonarQube monitoring for macOS', 'description' => 'Download the latest signed SonarDesk release for macOS, review release notes and system requirements.'])
+@section('fullbleed')
+{{ $settings['eyebrow'] }} {{ $settings['title'] }} {{ $settings['intro'] }}
@if($latest)
✓ Signed distribution ✓ SHA-256 verified ✓ Secure licence activation
@else
The next SonarDesk release is being prepared. Check back shortly or contact support.
@endif
SonarDesk
Project health All systems monitored
Native Built for macOS
+Native macOS Fast, focused desktop workflow
SonarQube Your server remains the source of truth
Security Tokens protected by macOS Keychain
Updates Professional in-app release delivery
+@if($latest)Latest stable release SonarDesk {{ $latest->version }} Build {{ $latest->build_number }} · Released {{ $latest->published_at?->format('j F Y') }}
Download {{ $latest->version }} {!! nl2br(e($latest->release_notes ?: 'This release contains the latest SonarDesk improvements and maintenance updates.')) !!}
SHA-256 {{ $latest->sha256 }}
@endif
+Release archive Previous versions Published SonarDesk builds remain available here for customers who need a previous release.
@forelse($releases as $release)
SonarDesk {{ $release->version }} Build {{ $release->build_number }} · {{ ucfirst($release->channel) }} · {{ $release->published_at?->format('j M Y') }}
@empty
No published releases are available yet.
@endforelse
{{ $releases->links() }}
+
+@endsection
diff --git a/sonar-desk-licensing/resources/views/emails/license-key-text.blade.php b/sonar-desk-licensing/resources/views/emails/license-key-text.blade.php
index cdaf808..d6b0a09 100644
--- a/sonar-desk-licensing/resources/views/emails/license-key-text.blade.php
+++ b/sonar-desk-licensing/resources/views/emails/license-key-text.blade.php
@@ -18,3 +18,6 @@ To activate:
@endif
Keep this key private.
+
+DOWNLOAD SONARDESK
+{{ $downloadUrl }}
diff --git a/sonar-desk-licensing/resources/views/emails/license-key.blade.php b/sonar-desk-licensing/resources/views/emails/license-key.blade.php
index bc23ef9..0a052a4 100644
--- a/sonar-desk-licensing/resources/views/emails/license-key.blade.php
+++ b/sonar-desk-licensing/resources/views/emails/license-key.blade.php
@@ -13,6 +13,7 @@
Open SonarDesk on your Mac. Open Settings → Licence . Paste the licence key above and select Activate .
@if($expiresAt)Current entitlement period: {{ $expiresAt->format('j M Y') }} .
@endif
@if($portalUrl)Manage your licence
@endif
+Download SonarDesk
Keep this key private. If you did not make this purchase, contact SonarDesk support.
diff --git a/sonar-desk-licensing/resources/views/layouts/app.blade.php b/sonar-desk-licensing/resources/views/layouts/app.blade.php
index 8f376d5..4dbe278 100755
--- a/sonar-desk-licensing/resources/views/layouts/app.blade.php
+++ b/sonar-desk-licensing/resources/views/layouts/app.blade.php
@@ -169,7 +169,14 @@
.admin-ticket-list{display:grid;gap:10px}.admin-ticket-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;gap:18px;align-items:center;padding:16px 18px;background:#fff;border:1px solid var(--line);border-radius:14px;text-decoration:none;box-shadow:var(--shadow-sm)}.admin-ticket-row:hover{border-color:#b8cdf6}.admin-ticket-row>div:first-child{display:flex;flex-direction:column;min-width:0}.admin-ticket-row small{color:var(--muted)}.ticket-ref{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--blue);font-weight:850}.ticket-category,.ticket-status{padding:5px 8px;border-radius:999px;font-size:12px;font-weight:800;background:#f2f4f7;color:#475467}.status-new{background:#eff6ff;color:#1d4ed8}.status-open{background:#ecfdf3;color:#067647}.status-waiting{background:#fff7ed;color:#9a3412}.status-resolved,.status-closed{background:#f2f4f7;color:#475467}.admin-ticket-row time{color:var(--muted);font-size:12px}.filter-bar{display:flex;align-items:center;gap:10px;margin-bottom:18px}.filter-bar select{width:auto;margin:0}.ticket-detail-head{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:22px}.ticket-detail-head h1{font-size:34px;line-height:1.12;letter-spacing:-.03em;margin:6px 0}.ticket-detail-head p{margin:0;color:var(--muted)}.ticket-message .ticket-meta{display:flex;gap:12px;color:var(--muted);font-size:13px;margin:8px 0 20px}.message-body{white-space:normal;background:#f8fafc;border:1px solid var(--line);border-radius:12px;padding:18px;line-height:1.7;margin-bottom:18px}
@media(max-width:800px){.support-layout{grid-template-columns:1fr}.form-grid.two{grid-template-columns:1fr}.legal-page{padding:28px 22px}.legal-table>div,.cookie-table>div{grid-template-columns:1fr;gap:4px}.cookie-banner-grid{grid-template-columns:1fr}.cookie-actions{justify-content:flex-start}.admin-ticket-row{grid-template-columns:1fr auto}.admin-ticket-row .ticket-category,.admin-ticket-row time{display:none}.ticket-detail-head{flex-direction:column}}
-
+
+ /* SonarDesk distribution centre */
+ .download-page{background:#f6f8fc;min-height:80vh}.download-shell{max-width:1220px;margin:0 auto;padding-left:24px;padding-right:24px}.download-hero{position:relative;overflow:hidden;background:radial-gradient(circle at 78% 22%,rgba(37,99,235,.22),transparent 32%),linear-gradient(135deg,#07111f 0%,#0b1730 55%,#101c35 100%);color:#fff;padding:92px 0 78px}.download-hero:after{content:'';position:absolute;inset:auto -10% -220px 35%;height:420px;background:rgba(59,130,246,.08);filter:blur(60px);border-radius:50%}.download-hero .download-shell{display:grid;grid-template-columns:1.02fr .98fr;gap:70px;align-items:center;position:relative;z-index:1}.download-copy h1{font-size:68px;line-height:.98;letter-spacing:-.055em;margin:12px 0 22px}.download-copy>p{font-size:20px;line-height:1.65;color:#cbd5e1;max-width:660px}.download-actions{display:flex;gap:12px;align-items:stretch;margin:32px 0 20px;flex-wrap:wrap}.download-primary{background:#fff!important;color:#0f172a!important;padding:13px 18px!important;gap:12px;text-align:left;box-shadow:0 16px 40px rgba(0,0,0,.22)}.download-primary:hover{transform:translateY(-1px)}.download-primary small{display:block;color:#64748b;font-size:11px;margin-top:3px}.download-icon{width:32px;height:32px;display:grid;place-items:center;border-radius:8px;background:#2563eb;color:#fff;font-size:20px}.download-secondary{background:rgba(255,255,255,.07)!important;border:1px solid rgba(255,255,255,.18)!important;color:#fff!important}.release-verification{display:flex;gap:18px;flex-wrap:wrap;color:#94a3b8;font-size:12px}.release-verification span:First-letter{color:#86efac}.product-visual{position:relative;perspective:1000px}.mac-window{border:1px solid rgba(255,255,255,.15);background:#111827;border-radius:18px;box-shadow:0 36px 90px rgba(0,0,0,.45);overflow:hidden;transform:rotateY(-5deg) rotateX(2deg)}.mac-titlebar{height:42px;background:#1b2434;display:flex;align-items:center;gap:7px;padding:0 14px;border-bottom:1px solid rgba(255,255,255,.06)}.mac-titlebar i{width:10px;height:10px;border-radius:50%;background:#fb7185}.mac-titlebar i:nth-child(2){background:#fbbf24}.mac-titlebar i:nth-child(3){background:#4ade80}.mac-titlebar span{margin-left:auto;margin-right:auto;color:#94a3b8;font-size:11px}.mac-content{display:grid;grid-template-columns:110px 1fr;height:310px}.mini-sidebar{background:#0b1220;padding:18px 12px;display:flex;flex-direction:column;gap:12px}.mini-sidebar img{width:30px;border-radius:8px}.mini-sidebar b{font-size:10px}.mini-sidebar span{height:9px;border-radius:4px;background:#26344c;width:78%}.mini-sidebar span.active{background:#2563eb;width:90%}.mini-dashboard{padding:24px}.mini-head{display:flex;justify-content:space-between;align-items:center}.mini-head b{font-size:14px}.mini-head em{font-size:8px;color:#4ade80;font-style:normal}.mini-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;margin:20px 0}.mini-stats i{height:58px;border:1px solid #273449;border-radius:8px;background:linear-gradient(145deg,#162033,#101827)}.mini-chart{height:130px;border:1px solid #273449;border-radius:9px;display:flex;align-items:flex-end;gap:9px;padding:16px}.mini-chart span{flex:1;background:linear-gradient(#3b82f6,#1d4ed8);border-radius:4px 4px 2px 2px}.mini-chart span:nth-child(1){height:35%}.mini-chart span:nth-child(2){height:58%}.mini-chart span:nth-child(3){height:45%}.mini-chart span:nth-child(4){height:78%}.mini-chart span:nth-child(5){height:66%}.mini-chart span:nth-child(6){height:90%}.visual-badge{position:absolute;right:-20px;bottom:-22px;background:#fff;color:#0f172a;border-radius:12px;padding:12px 18px;box-shadow:0 14px 34px rgba(0,0,0,.3)}.visual-badge span{display:block;font-size:9px;color:#2563eb;text-transform:uppercase;font-weight:900;letter-spacing:.12em}.visual-badge strong{font-size:13px}.download-confidence{background:#fff;border-bottom:1px solid #e5e7eb}.confidence-grid{display:grid;grid-template-columns:repeat(4,1fr);padding-top:25px;padding-bottom:25px}.confidence-grid div{padding:0 24px;border-right:1px solid #e5e7eb}.confidence-grid div:first-child{padding-left:0}.confidence-grid div:last-child{border:0}.confidence-grid span{display:block;text-transform:uppercase;letter-spacing:.1em;font-size:9px;font-weight:900;color:#2563eb;margin-bottom:4px}.confidence-grid strong{font-size:12px;color:#344054}.download-content{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(280px,.6fr);gap:28px;padding-top:72px;padding-bottom:72px}.release-notes-panel,.requirements-panel{background:#fff;border:1px solid #e4e7ec;border-radius:20px;padding:34px;box-shadow:0 12px 35px rgba(15,23,42,.05)}.release-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;border-bottom:1px solid #eaecf0;padding-bottom:24px}.release-heading h2,.requirements-panel h2{font-size:30px;letter-spacing:-.03em;margin:7px 0 5px}.release-heading p{margin:0;color:#667085}.release-note-copy{padding:28px 0;color:#475467;font-size:16px;line-height:1.8}.checksum-box{background:#f8fafc;border:1px solid #e5e7eb;border-radius:10px;padding:14px}.checksum-box span{display:block;font-size:9px;font-weight:900;letter-spacing:.12em;color:#64748b;margin-bottom:5px}.checksum-box code{font-size:11px;overflow-wrap:anywhere;color:#334155}.requirements-panel p{color:#667085}.requirements-panel ol{list-style:none;padding:15px 0;margin:0}.requirements-panel li{display:flex;gap:12px;padding:13px 0;border-top:1px solid #eef2f6}.requirements-panel li>span{display:grid;place-items:center;width:28px;height:28px;background:#eff6ff;color:#2563eb;border-radius:8px;font-weight:900;font-size:11px}.requirements-panel li strong,.requirements-panel li small{display:block}.requirements-panel li small{color:#667085;margin-top:2px}.requirements-panel>a{color:#2563eb;font-weight:800;text-decoration:none;font-size:13px}.release-history{background:#fff;border-top:1px solid #e5e7eb;padding:70px 0}.release-history-list{border:1px solid #e5e7eb;border-radius:16px;overflow:hidden}.release-history-list article{display:flex;justify-content:space-between;align-items:center;padding:18px 22px;border-bottom:1px solid #e5e7eb}.release-history-list article:last-child{border:0}.release-history-list strong,.release-history-list span{display:block}.release-history-list span{font-size:12px;color:#667085;margin-top:3px}.release-history-list article>div:last-child{display:flex;align-items:center;gap:20px}.release-history-list a{color:#2563eb;font-weight:800;text-decoration:none}.download-support{background:#0b1220;color:#fff;padding:55px 0}.download-support .download-shell{display:flex;align-items:center;justify-content:space-between;gap:30px}.download-support h2{font-size:32px;margin:7px 0}.download-support p{color:#94a3b8;margin:0}.no-release{margin-top:28px;padding:18px;border:1px solid rgba(255,255,255,.15);border-radius:12px;background:rgba(255,255,255,.06)}.no-release span{display:block;color:#94a3b8;margin-top:3px}
+ .admin-release-head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px}.admin-metrics{margin-bottom:24px}.admin-table-card,.editor-card{background:#fff;border:1px solid #e4e7ec;border-radius:16px;box-shadow:var(--shadow-sm)}.admin-table-title{display:flex;justify-content:space-between;align-items:center;padding:22px 24px;border-bottom:1px solid #e4e7ec}.admin-table-title h2{margin:0;font-size:19px}.admin-table-title p{margin:3px 0 0;color:#667085;font-size:13px}.table-scroll{overflow-x:auto}.pro-table{width:100%;border-collapse:collapse}.pro-table th{background:#f8fafc;text-align:left;padding:11px 16px;color:#667085;font-size:10px;text-transform:uppercase;letter-spacing:.08em}.pro-table td{padding:15px 16px;border-top:1px solid #eef2f6;vertical-align:middle}.pro-table td strong,.pro-table td small{display:block}.pro-table td small{color:#667085;margin-top:3px}.table-actions{display:flex;gap:7px;justify-content:flex-end}.table-actions form{margin:0}.status-chip{display:inline-flex;padding:4px 8px;border-radius:99px;background:#eff6ff;color:#1d4ed8;font-size:10px;font-weight:850}.status-chip.success{background:#ecfdf3;color:#067647}.status-chip.muted{background:#f2f4f7;color:#667085}.empty-cell{text-align:center!important;color:#667085;padding:40px!important}.release-editor{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:22px;align-items:start}.editor-main{display:grid;gap:18px}.editor-card{padding:24px}.editor-card-head{display:flex;gap:13px;margin-bottom:22px}.editor-card-head>span{display:grid;place-items:center;width:34px;height:34px;border-radius:9px;background:#eff6ff;color:#2563eb;font-size:10px;font-weight:900}.editor-card h2{margin:0;font-size:19px}.editor-card p{margin:3px 0 0;color:#667085;font-size:13px}.form-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}.editor-card label,.settings-editor label{display:grid;gap:7px;font-size:12px;font-weight:800;color:#344054}.editor-card input,.editor-card select,.editor-card textarea,.settings-editor input,.settings-editor textarea{width:100%;border:1px solid #d0d5dd;border-radius:9px;padding:10px 12px;background:#fff;color:#101828;font:inherit}.upload-zone{padding:22px!important;border:1px dashed #98a2b3;border-radius:12px;background:#f9fafb;margin-bottom:18px}.upload-zone span{color:#667085;font-weight:500}.upload-zone input{border:0!important;padding:10px 0 0!important;background:transparent!important}.package-current{padding:13px 15px;border-radius:10px;background:#ecfdf3;margin-bottom:14px}.package-current strong,.package-current span{display:block}.package-current span{font-size:10px;color:#067647;overflow-wrap:anywhere;margin-top:4px}.sticky-card{position:sticky;top:92px}.toggle-row{display:flex!important;align-items:flex-start;gap:10px!important;padding:14px 0;border-top:1px solid #eef2f6}.toggle-row input{width:auto!important;margin-top:3px}.toggle-row strong,.toggle-row small{display:block}.toggle-row small{color:#667085;margin-top:2px;font-weight:500}.sticky-card .btn{margin-top:10px}.settings-editor{max-width:820px;display:grid;gap:18px}
+ @media(max-width:900px){.download-hero .download-shell{grid-template-columns:1fr}.product-visual{display:none}.download-copy h1{font-size:52px}.confidence-grid{grid-template-columns:repeat(2,1fr);gap:20px}.confidence-grid div{border:0;padding:0}.download-content,.release-editor{grid-template-columns:1fr}.sticky-card{position:static}.download-support .download-shell,.admin-release-head{align-items:flex-start;flex-direction:column}}@media(max-width:600px){.download-hero{padding:64px 0}.download-copy h1{font-size:42px}.download-actions,.release-heading{flex-direction:column}.download-primary,.download-secondary{width:100%}.confidence-grid{grid-template-columns:1fr}.release-history-list article{align-items:flex-start;gap:12px;flex-direction:column}.download-content{padding-top:40px;padding-bottom:40px}.release-notes-panel,.requirements-panel{padding:22px}.form-grid{grid-template-columns:1fr}.download-support .download-shell{align-items:stretch}}
+.portal-download-banner{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:18px 20px;margin-bottom:22px;background:linear-gradient(135deg,#0f172a,#172554);color:#fff;border-radius:14px;box-shadow:var(--shadow-md)}.portal-download-banner strong,.portal-download-banner small{display:block}.portal-download-banner strong{font-size:18px;margin:3px 0}.portal-download-banner small{color:#cbd5e1}.portal-download-banner .eyebrow{color:#93c5fd}@media(max-width:650px){.portal-download-banner{align-items:stretch;flex-direction:column}}
+.home-download-showcase{display:grid;grid-template-columns:1.25fr .75fr;gap:36px;align-items:center;margin:0 0 64px;padding:38px;background:linear-gradient(135deg,#f8fafc,#eff6ff);border:1px solid #dbeafe;border-radius:22px}.home-download-copy h2{font-size:38px;letter-spacing:-.035em;margin:7px 0 10px}.home-download-copy p{color:#667085;font-size:16px;max-width:680px}.home-download-meta{background:#0b1220;color:#fff;border-radius:16px;padding:24px;box-shadow:0 18px 40px rgba(15,23,42,.16)}.home-download-meta>span{display:block;color:#93c5fd;text-transform:uppercase;font-size:9px;font-weight:900;letter-spacing:.12em}.home-download-meta>strong{display:block;font-size:24px;margin:7px 0 3px}.home-download-meta>small{color:#94a3b8}.home-download-meta>div{margin-top:20px;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.home-download-meta b,.home-download-meta code{display:block}.home-download-meta b{font-size:9px;color:#64748b;letter-spacing:.1em}.home-download-meta code{font-size:10px;color:#cbd5e1;margin-top:5px}@media(max-width:760px){.home-download-showcase{grid-template-columns:1fr;padding:24px}}
+
@stack('head')
@@ -183,7 +190,7 @@
Product
Features
Pricing
- Release
+ Download
Support
Privacy
@auth
@@ -200,6 +207,7 @@
@endphp
Admin
+ Releases
Support
@if ($supportUnread > 0)
@@ -241,10 +249,11 @@
Product
Features
Pricing
- Release
+ Download
Support
Privacy
Terms
+ Refunds
Cookies
@auth @if(auth()->user()->hasVerifiedEmail())Licence portal @endif @elseSign in @endauth
diff --git a/sonar-desk-licensing/resources/views/legal/refund.blade.php b/sonar-desk-licensing/resources/views/legal/refund.blade.php
new file mode 100644
index 0000000..4410517
--- /dev/null
+++ b/sonar-desk-licensing/resources/views/legal/refund.blade.php
@@ -0,0 +1,102 @@
+@extends('layouts.app', ['title' => 'Refund Policy — SonarDesk'])
+
+@section('content')
+@php($legal = config('sonardesk.legal'))
+
+
+
+
+ This policy explains cancellation, refund and statutory consumer rights for SonarDesk subscriptions.
+ SonarDesk payments are processed by Paddle, our Merchant of Record. Nothing in this policy limits any
+ rights or remedies that cannot lawfully be limited.
+
+
+
+ 1. Seven-day free trial
+ Where shown at checkout, SonarDesk includes a seven-day free trial. You can cancel before the trial
+ ends to prevent the subscription converting to a paid subscription. If you cancel during the trial
+ before a charge is made, there is no subscription payment to refund.
+
+
+
+ 2. Cancelling a paid subscription
+ You may cancel your subscription through the SonarDesk licence portal or the subscription-management
+ facilities provided through Paddle. Cancellation stops future automatic renewals. Unless stated
+ otherwise during cancellation, your licence normally remains available until the end of the billing
+ period for which you have already paid.
+ Cancellation does not itself create a refund for an earlier payment.
+
+
+
+ 3. Refund requests
+ If you believe a payment should be refunded, contact
+ SonarDesk Support as soon as possible. Please provide the email
+ address used for the purchase and enough information for us to identify the transaction. Do not send
+ full payment-card details.
+ Because Paddle is the Merchant of Record, a refund may be processed by Paddle and may also be
+ requested through Paddle's buyer-support facilities. Approved refunds are returned to the original
+ payment method in accordance with Paddle's processing arrangements and the payment provider's timing.
+
+
+
+ 4. UK and EEA consumer withdrawal rights
+ If you purchase as a consumer, applicable law may provide a cooling-off or withdrawal period for
+ distance contracts. For UK consumers this is generally 14 days, subject to the statutory rules and
+ exceptions that apply to digital content and services.
+ If you expressly request or consent to digital supply or performance beginning during a statutory
+ withdrawal period, the law may affect the right to withdraw once supply or performance has begun.
+ Nothing on this page removes a statutory right that applies to your purchase.
+
+
+
+ 5. Faulty or misdescribed digital content
+ This refund policy does not replace your statutory remedies. If SonarDesk is faulty, materially not
+ as described, or otherwise fails to meet rights that apply to digital content or services under
+ consumer law, you may be entitled to a repair, replacement, price reduction, refund or another remedy
+ depending on the circumstances.
+
+
+
+ 6. Duplicate or incorrect charges
+ If you believe you have been charged twice, charged after a valid cancellation, charged an incorrect
+ amount, or charged for the wrong plan, contact us promptly. We will investigate the subscription and
+ transaction records and, where appropriate, arrange correction or refund through Paddle.
+
+
+
+ 7. Renewal payments
+ Subscriptions renew automatically until cancelled. A renewal charge is not automatically refundable
+ solely because the subscription was not cancelled before renewal. We will nevertheless consider refund
+ requests fairly, taking account of the circumstances, applicable law, the timing of the request and
+ whether the renewed service has been used.
+
+
+
+ 8. Chargebacks
+ Please contact SonarDesk Support or Paddle about a disputed payment before initiating a chargeback
+ where practical. This gives us an opportunity to investigate duplicate, fraudulent or incorrect charges
+ promptly. This does not restrict any right you have to dispute a payment with your payment provider.
+
+
+
+ 9. How refunds affect your licence
+ A full refund or reversal of a subscription payment may cause the associated subscription and
+ SonarDesk licence to be cancelled or revoked. Where only part of a payment is refunded, any effect on
+ the subscription will depend on the reason for the adjustment and the resulting Paddle subscription
+ status.
+
+
+
+
+@endsection
diff --git a/sonar-desk-licensing/resources/views/legal/terms.blade.php b/sonar-desk-licensing/resources/views/legal/terms.blade.php
index db09d91..45c57de 100755
--- a/sonar-desk-licensing/resources/views/legal/terms.blade.php
+++ b/sonar-desk-licensing/resources/views/legal/terms.blade.php
@@ -3,17 +3,144 @@
@section('content')
@php($legal = config('sonardesk.legal'))
-
- These terms govern your use of the SonarDesk website, licence portal, subscription service and macOS application.
- 1. Licence plans Personal permits the number of active Mac installations shown for that plan at purchase. Professional permits the number shown for Professional. A licence is for your use and may not be shared, resold or used to bypass activation limits.
- 2. Trials and subscriptions Where offered, the trial length and renewal terms are shown at checkout. Subscription billing, cancellation, tax and payment processing are handled through Paddle. Your subscription continues until cancelled in accordance with the checkout terms. Changes between Personal and Professional may be prorated by Paddle.
- 3. Activation SonarDesk uses a licence service to activate permitted Macs and periodically confirm that an activation remains valid. A reasonable offline grace period may be provided, but continued use after a subscription or activation is revoked is not authorised.
- 4. Your SonarQube connections You are responsible for the SonarQube servers, credentials, tokens and projects you connect to SonarDesk. SonarDesk displays information made available by your SonarQube account and does not grant additional server permissions.
- 5. Acceptable use You must not attempt to defeat licensing controls, interfere with the service, probe accounts you do not own, distribute malware, or use SonarDesk in violation of applicable law or third-party rights.
- 6. Updates and availability We may update the application, website and licensing service to improve security, compatibility or functionality. We aim to provide a reliable service but do not guarantee uninterrupted availability.
- 7. Consumer rights Nothing in these terms excludes rights that cannot lawfully be excluded, including applicable UK consumer rights. Where mandatory law gives you stronger protections, that law prevails.
- 8. Liability To the extent permitted by law, we are not responsible for indirect or consequential loss arising from incorrect sync configuration, deleted destination data, unavailable third-party systems or failures outside our reasonable control. This does not limit liability that cannot lawfully be limited.
-
- 10. Contact These terms are provided by {{ $legal['controller_name'] }} in {{ $legal['country'] }}. Questions can be submitted through SonarDesk Support .
+
+
+
+ These Terms of Service govern your use of the SonarDesk website, licence portal,
+ subscription service and macOS application. By purchasing, activating or using SonarDesk,
+ you agree to these terms. Your statutory rights are not affected.
+
+
+
+ 1. SonarDesk licences
+ SonarDesk is licensed, not sold. A Personal subscription permits the number of active Mac
+ installations stated for that plan at the time of purchase. A Professional subscription permits
+ the number stated for the Professional plan. Licences are for the subscriber's use and must not
+ be shared, resold, sublicensed or used to circumvent activation limits.
+
+
+
+ 2. Free trials
+ Where a free trial is offered, its duration and the price that will apply afterwards are shown
+ before checkout. Unless you cancel before the trial ends, the subscription will automatically
+ convert to the selected paid plan and Paddle will charge the payment method provided at checkout.
+
+
+
+ 3. Subscriptions, billing and renewal
+ SonarDesk subscriptions renew automatically at the billing interval selected at checkout until
+ cancelled. The renewal price, billing period, applicable taxes and other checkout information are
+ presented before you complete the transaction. Payment processing, tax collection and subscription
+ billing are handled by Paddle, our Merchant of Record.
+ Changes between Personal and Professional plans may result in a prorated charge or credit as
+ calculated by Paddle and displayed as part of the subscription change.
+
+
+
+ 4. Cancellation
+ You may cancel a subscription through the SonarDesk licence portal or the subscription-management
+ facilities made available through Paddle. Cancellation prevents future renewals. Unless the
+ cancellation flow expressly states otherwise, access normally continues until the end of the
+ already-paid billing period.
+ Cancellation and refund requests are separate. Cancelling a subscription does not automatically
+ refund charges already paid. See our Refund Policy .
+
+
+
+ 5. Activation and licence checks
+ SonarDesk uses a licence service to activate permitted Macs and periodically confirm that an
+ activation remains valid. A reasonable offline grace period may be provided. Continued use after a
+ licence or subscription has expired or been revoked is not authorised.
+
+
+
+ 6. Your SonarQube connections
+ You are responsible for the SonarQube servers, credentials, tokens and projects you connect to
+ SonarDesk. SonarDesk accesses information made available by the permissions of the credentials you
+ provide. It does not grant additional permissions on your SonarQube server.
+ You are responsible for ensuring that you are authorised to access and process the projects and
+ information you connect to SonarDesk.
+
+
+
+ 7. Acceptable use
+ You must not attempt to defeat or circumvent licensing controls, interfere with the SonarDesk
+ service, gain unauthorised access to accounts or systems, distribute malware through the service,
+ reverse engineer the service except where applicable law expressly permits it, or use SonarDesk in
+ violation of applicable law or third-party rights.
+
+
+
+ 8. Software updates and service availability
+ We may update SonarDesk, the website and licensing service to improve security, compatibility,
+ reliability or functionality. We may also discontinue obsolete versions where reasonably necessary.
+ We aim to provide a reliable service but do not guarantee uninterrupted or error-free availability.
+
+
+
+ 9. Third-party services
+ SonarDesk works with third-party services including SonarQube and Paddle. Those services are
+ operated independently and may have their own terms and policies. We are not responsible for changes,
+ outages or restrictions imposed by third-party services outside our reasonable control.
+
+
+
+ 10. Consumer rights
+ Nothing in these terms excludes or restricts rights that cannot lawfully be excluded or restricted.
+ If you are a consumer, this includes applicable rights relating to digital content and services under
+ UK consumer law. Where mandatory law gives you stronger protection than these terms, the mandatory
+ protection applies.
+ Information about refunds, statutory remedies and withdrawal rights is set out in our
+ Refund Policy .
+
+
+
+ 11. Liability
+ Nothing in these terms excludes or limits liability where doing so would be unlawful, including
+ liability for death or personal injury caused by negligence, fraud or fraudulent misrepresentation.
+ Subject to those protections, we are not responsible for indirect or consequential losses, loss
+ caused by unauthorised or incorrect configuration, unavailable third-party systems, or events outside
+ our reasonable control. If you use SonarDesk for business purposes, you are responsible for maintaining
+ appropriate backups and operational safeguards.
+
+
+
+ 12. Suspension and termination
+ We may suspend or terminate access where reasonably necessary to address fraud, security threats,
+ material breach of these terms, chargebacks, or an invalid or expired subscription. Where appropriate,
+ we will give reasonable notice and an opportunity to resolve the issue.
+
+
+
+ 13. Changes to these terms
+ We may update these terms when the service, law or our commercial arrangements change. The current
+ version will be published on this page with its revision date. Material changes affecting an existing
+ subscription will be communicated where required by law.
+
+
+
+
+
+ 15. Governing law
+ These terms are governed by the laws applicable in {{ $legal['country'] }}, subject to any mandatory
+ consumer protections and rights you have under the law of the country in which you live.
+
+
+
@endsection
diff --git a/sonar-desk-licensing/resources/views/portal/index.blade.php b/sonar-desk-licensing/resources/views/portal/index.blade.php
index 0a874fb..5424076 100755
--- a/sonar-desk-licensing/resources/views/portal/index.blade.php
+++ b/sonar-desk-licensing/resources/views/portal/index.blade.php
@@ -1,6 +1,7 @@
@extends('layouts.app')
@section('content')
+Latest SonarDesk Need the Mac app? Download the latest published, signed release from the SonarDesk distribution centre.
Download SonarDesk