SEO additions
This commit is contained in:
@@ -1,91 +1,301 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="hero-card">
|
<div class="hero-card">
|
||||||
<span class="eyebrow">SonarDesk</span>
|
<span class="eyebrow">SonarDesk</span>
|
||||||
<h1>Choose your licence</h1>
|
<h1>Choose your licence</h1>
|
||||||
<p class="lead">Every subscription starts with a 7-day free trial. Cancel before the trial ends and you won't be charged.</p>
|
<p class="lead">
|
||||||
@if($paddleEnvironment === 'sandbox')<div class="sandbox-badge">Sandbox checkout — no real payment is taken</div>@endif
|
Every subscription starts with a 7-day free trial.
|
||||||
</div>
|
Cancel before the trial ends and you won't be charged.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="pricing-grid">
|
@if($paddleEnvironment === 'sandbox')
|
||||||
@foreach($plans as $code => $plan)
|
<div class="sandbox-badge">
|
||||||
<article class="price-card {{ str_starts_with($code, 'professional') ? 'featured' : '' }}">
|
Sandbox checkout — no real payment is taken
|
||||||
<div>
|
|
||||||
<div class="plan-heading">
|
|
||||||
<div>
|
|
||||||
<h2>{{ $plan['name'] }}</h2>
|
|
||||||
<div class="muted">{{ $plan['billing'] }} billing</div>
|
|
||||||
</div>
|
|
||||||
@if($plan['billing'] === 'Annual')<span class="pill">Best value</span>@endif
|
|
||||||
</div>
|
</div>
|
||||||
<div class="price" data-price-display="{{ $plan['price_id'] }}" data-billing="{{ strtolower($plan['billing']) }}">{{ $plan['display_price'] }}</div>
|
@endif
|
||||||
<ul class="features">
|
</div>
|
||||||
<li>7-day free trial</li>
|
|
||||||
<li>{{ $plan['activation_limit'] }} active Mac{{ $plan['activation_limit'] === 1 ? '' : 's' }}</li>
|
|
||||||
<li>Secure online activation</li>
|
|
||||||
<li>Licence management portal</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<button class="checkout-button" type="button" data-price-id="{{ $plan['price_id'] }}" data-plan="{{ $plan['name'] }} {{ $plan['billing'] }}">Start free trial</button>
|
|
||||||
</article>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card checkout-status" id="checkout-status" hidden>
|
<div class="pricing-grid">
|
||||||
<h2>Purchase received</h2>
|
@foreach($plans as $code => $plan)
|
||||||
<p>Paddle is confirming your subscription. Your SonarDesk licence key is generated by our licensing server and emailed to the address used at checkout. You can then activate it in <strong>SonarDesk → Settings → Licence</strong>.</p>
|
<article class="price-card {{ str_starts_with($code, 'professional') ? 'featured' : '' }}">
|
||||||
<p class="muted">Delivery normally follows the subscription webhook within moments. Check your junk folder if needed. You can also create a portal account with the same email address and resend the key from My licences.</p>
|
<div>
|
||||||
<a class="btn" href="{{ route('register') }}">Create portal account</a>
|
<div class="plan-heading">
|
||||||
</div>
|
<div>
|
||||||
|
<h2>{{ $plan['name'] }}</h2>
|
||||||
|
<div class="muted">{{ $plan['billing'] }} billing</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
|
@if($plan['billing'] === 'Annual')
|
||||||
<script>
|
<span class="pill">Best value</span>
|
||||||
(() => {
|
@endif
|
||||||
const token = @json($paddleToken);
|
</div>
|
||||||
const environment = @json($paddleEnvironment);
|
|
||||||
if (environment === 'sandbox') Paddle.Environment.set('sandbox');
|
|
||||||
Paddle.Initialize({
|
|
||||||
token,
|
|
||||||
eventCallback(event) {
|
|
||||||
if (event && event.name === 'checkout.completed') {
|
|
||||||
document.getElementById('checkout-status').hidden = false;
|
|
||||||
document.getElementById('checkout-status').scrollIntoView({behavior: 'smooth', block: 'center'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const priceElements = new Map(
|
<div
|
||||||
Array.from(document.querySelectorAll('[data-price-display]')).map(element => [element.dataset.priceDisplay, element])
|
class="price"
|
||||||
);
|
data-price-display="{{ $plan['price_id'] }}"
|
||||||
const previewItems = Array.from(priceElements.keys()).map(priceId => ({priceId, quantity: 1}));
|
data-billing="{{ strtolower($plan['billing']) }}"
|
||||||
Paddle.PricePreview({items: previewItems}).then(result => {
|
>
|
||||||
for (const item of (result?.data?.details?.lineItems ?? [])) {
|
{{ $plan['display_price'] }}
|
||||||
const element = priceElements.get(item?.price?.id);
|
</div>
|
||||||
const formatted = item?.formattedTotals?.total ?? item?.formattedUnitTotals?.total;
|
|
||||||
if (!element || !formatted) continue;
|
|
||||||
const suffix = element.dataset.billing === 'annual' ? ' / year' : ' / month';
|
|
||||||
element.textContent = formatted + suffix;
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
// Keep the server-rendered GBP fallback if Paddle pricing preview is unavailable.
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.checkout-button').forEach(button => {
|
<ul class="features">
|
||||||
button.addEventListener('click', () => {
|
<li>7-day free trial</li>
|
||||||
Paddle.Checkout.open({
|
<li>
|
||||||
items: [{priceId: button.dataset.priceId, quantity: 1}],
|
{{ $plan['activation_limit'] }}
|
||||||
settings: {
|
active Mac{{ $plan['activation_limit'] === 1 ? '' : 's' }}
|
||||||
displayMode: 'overlay',
|
</li>
|
||||||
theme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
|
<li>Secure online activation</li>
|
||||||
allowLogout: false
|
<li>Licence management portal</li>
|
||||||
},
|
</ul>
|
||||||
customData: {
|
</div>
|
||||||
source: 'sonardesk-buy-page',
|
|
||||||
plan: button.dataset.plan
|
<button
|
||||||
|
class="checkout-button"
|
||||||
|
type="button"
|
||||||
|
data-price-id="{{ $plan['price_id'] }}"
|
||||||
|
data-plan="{{ $plan['name'] }} {{ $plan['billing'] }}"
|
||||||
|
>
|
||||||
|
Start free trial
|
||||||
|
</button>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card checkout-status" id="checkout-status" hidden>
|
||||||
|
<h2>Purchase received</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Paddle is confirming your subscription. Your SonarDesk licence key
|
||||||
|
is generated by our licensing server and emailed to the address used
|
||||||
|
at checkout. You can then activate it in
|
||||||
|
<strong>SonarDesk → Settings → Licence</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="muted">
|
||||||
|
Delivery normally follows the subscription webhook within moments.
|
||||||
|
Check your junk folder if needed. You can also create a portal account
|
||||||
|
with the same email address and resend the key from My licences.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a class="btn" href="{{ route('register') }}">
|
||||||
|
Create portal account
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="checkout-error" hidden>
|
||||||
|
<h2>Checkout unavailable</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Paddle could not start the checkout. Please try again.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="muted" id="checkout-error-detail"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const token = @json($paddleToken);
|
||||||
|
const environment = @json($paddleEnvironment);
|
||||||
|
|
||||||
|
const errorPanel = document.getElementById('checkout-error');
|
||||||
|
const errorDetail = document.getElementById('checkout-error-detail');
|
||||||
|
|
||||||
|
function showCheckoutError(message) {
|
||||||
|
console.error('SONARDESK CHECKOUT ERROR:', message);
|
||||||
|
|
||||||
|
if (errorDetail) {
|
||||||
|
errorDetail.textContent = message || 'Unknown checkout error.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errorPanel) {
|
||||||
|
errorPanel.hidden = false;
|
||||||
|
|
||||||
|
errorPanel.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logPaddleEvent(event) {
|
||||||
|
console.log('PADDLE EVENT:', event);
|
||||||
|
|
||||||
|
if (
|
||||||
|
event?.name === 'checkout.error' ||
|
||||||
|
event?.name === 'checkout.warning' ||
|
||||||
|
event?.name === 'checkout.payment.error'
|
||||||
|
) {
|
||||||
|
console.error('PADDLE ERROR:', {
|
||||||
|
name: event?.name,
|
||||||
|
type: event?.type,
|
||||||
|
code: event?.code,
|
||||||
|
detail: event?.detail,
|
||||||
|
documentation_url: event?.documentation_url,
|
||||||
|
errors: event?.errors,
|
||||||
|
event: event
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = event?.code
|
||||||
|
? `Code: ${event.code}`
|
||||||
|
: 'Paddle checkout error';
|
||||||
|
|
||||||
|
const detail = event?.detail
|
||||||
|
? ` — ${event.detail}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
showCheckoutError(code + detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event?.name === 'checkout.completed') {
|
||||||
|
const status = document.getElementById('checkout-status');
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
status.hidden = false;
|
||||||
|
|
||||||
|
status.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Paddle === 'undefined') {
|
||||||
|
showCheckoutError('Paddle.js failed to load.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('SONARDESK PADDLE CONFIG:', {
|
||||||
|
environment: environment,
|
||||||
|
tokenPresent: Boolean(token),
|
||||||
|
tokenPrefix: token ? token.substring(0, 5) : null
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
try {
|
||||||
})();
|
if (environment === 'sandbox') {
|
||||||
</script>
|
Paddle.Environment.set('sandbox');
|
||||||
|
}
|
||||||
|
|
||||||
|
Paddle.Initialize({
|
||||||
|
token: token,
|
||||||
|
eventCallback: logPaddleEvent
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Paddle initialized successfully.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PADDLE INITIALIZATION ERROR:', error);
|
||||||
|
|
||||||
|
showCheckoutError(
|
||||||
|
error?.message || 'Paddle could not be initialized.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceElements = new Map(
|
||||||
|
Array.from(
|
||||||
|
document.querySelectorAll('[data-price-display]')
|
||||||
|
).map(element => [
|
||||||
|
element.dataset.priceDisplay,
|
||||||
|
element
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const previewItems = Array.from(priceElements.keys()).map(priceId => ({
|
||||||
|
priceId: priceId,
|
||||||
|
quantity: 1
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('PADDLE PRICE PREVIEW ITEMS:', previewItems);
|
||||||
|
|
||||||
|
Paddle.PricePreview({
|
||||||
|
items: previewItems
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
console.log('PADDLE PRICE PREVIEW RESULT:', result);
|
||||||
|
|
||||||
|
for (const item of (result?.data?.details?.lineItems ?? [])) {
|
||||||
|
const element = priceElements.get(item?.price?.id);
|
||||||
|
|
||||||
|
const formatted =
|
||||||
|
item?.formattedTotals?.total ??
|
||||||
|
item?.formattedUnitTotals?.total;
|
||||||
|
|
||||||
|
if (!element || !formatted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix =
|
||||||
|
element.dataset.billing === 'annual'
|
||||||
|
? ' / year'
|
||||||
|
: ' / month';
|
||||||
|
|
||||||
|
element.textContent = formatted + suffix;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('PADDLE PRICE PREVIEW ERROR:', error);
|
||||||
|
|
||||||
|
// Server-rendered GBP price remains visible.
|
||||||
|
});
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll('.checkout-button')
|
||||||
|
.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const priceId = button.dataset.priceId;
|
||||||
|
const plan = button.dataset.plan;
|
||||||
|
|
||||||
|
console.log('OPENING PADDLE CHECKOUT:', {
|
||||||
|
environment: environment,
|
||||||
|
priceId: priceId,
|
||||||
|
plan: plan
|
||||||
|
});
|
||||||
|
|
||||||
|
errorPanel.hidden = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Paddle.Checkout.open({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
priceId: priceId,
|
||||||
|
quantity: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
displayMode: 'overlay',
|
||||||
|
theme: window.matchMedia(
|
||||||
|
'(prefers-color-scheme: dark)'
|
||||||
|
).matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light',
|
||||||
|
allowLogout: false
|
||||||
|
},
|
||||||
|
|
||||||
|
customData: {
|
||||||
|
source: 'sonardesk-buy-page',
|
||||||
|
plan: plan
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'PADDLE CHECKOUT OPEN ERROR:',
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
showCheckoutError(
|
||||||
|
error?.message ||
|
||||||
|
'Paddle could not open the checkout.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
Reference in New Issue
Block a user