Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 2026-10-31 — Mobile DevCon26 hero

- Keep the theme on two centred lines with a compact centred tagline, tighter introduction spacing and a centred content-sized ticket button on mobile. Centre the date and balance the wrapped venue text beneath it. Preserve the exact approved wording and desktop layout.

## 2026-10-31 — Homepage growth accent

- Highlight “grow.” in pink with a curved underline that reveals once on entering view. Preserve headline wording and use a static underline for reduced motion or unavailable JavaScript.

## 2026-10-31 — DevCon26 theme copy

- Replace the hero headline, tagline and introduction with the supplied Technology for the Next Generation wording.

## 2026-10-31 — Center checkout access recovery

- Center missing-browser-access guidance on the existing cream background with concise copy, a fixed 64 × 48 px ticket-and-envelope illustration, a plain duplicate-payment reminder, selectable payment reference and a contact email action carrying that reference. Keep retry and ticket details hidden, and clear recovery content when the confirmation state changes.

## 2026-10-30 — Polish private ticket claim controls

- Match the claim page's primary action to the DevCon26 checkout pink while keeping the styling scoped to ticket-access pages.
Expand Down Expand Up @@ -369,3 +385,8 @@
- Use an explicit two-column mobile quantity row so the label stays centered and the stepper sits at the right edge. Keep 44px button targets and allow the label to wrap on narrow screens.

- Added a softly masked community photo behind the expanded mobile order summary at 10% opacity, preserving text contrast and the compact collapsed state.

## 2026-10-30 — Checkout recovery

- Explain the 15-minute checkout hold and late-payment outcome.
- When a payment confirmation link has no browser grant, direct the buyer to their private ticket email instead of offering a Retry that cannot make a request; warn against paying again while payment may still complete.
6 changes: 6 additions & 0 deletions docs/devcon26-flexible-checkout.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,9 @@ Below 821px, progress uses the current step name, step count and three segments.
### Post-payment photo adjustment

The payment badge editor opens the photo adjuster after a new image is selected and exposes Adjust photo in both editing and preview steps. Zoom and horizontal/vertical position use the existing seat card crop API (`scale`, `x`, `y`). Saving waits for the selected seat's upload, persists its crop, and preserves retry controls on failure. Previously saved crops are restored when reopening the editor. Desktop uses side-by-side controls; mobile stacks the preview and 44px controls within a scrollable dialog.

## Failure recovery

An order that cannot accept payment retains its request identity; the buyer is directed to check email or contact the organizer before starting again. The payment confirmation page hides Retry if its browser grant is absent and directs the buyer to their private ticket email. This does not infer that payment failed or authorize a new charge. The admission backend separately reconciles stored payment references.

The missing-access state uses a centered, narrow layout on the existing cream background, with a fixed-size ticket-and-envelope illustration beside the heading on desktop and mobile. It displays the buyer-formatted reference as a support identifier and includes it in the contact-email subject; it is not proof of payment. Other confirmation states hide and clear these recovery details.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 19 additions & 2 deletions scripts/check-devcon26-managed-checkout.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,13 @@ for (const options of [{ missingGrant: true }, { responseMode: 'live' }]) {
const fixture = confirmationFixture(initialized('test').reference, 'test', options);

await tick();
assert.equal(fixture.root.dataset.paymentState, 'error');
assert.equal(fixture.root.dataset.paymentState, options.missingGrant ? 'access_lost' : 'error');
assert.equal(fixture.root.querySelector('[data-payment-receipt]').hidden, true);
assert.equal(fixture.badgeCalls.length, 0);
if (options.missingGrant) assert.equal(fixture.requests.length, 0);
if (options.missingGrant) {
assert.equal(fixture.requests.length, 0);
assert.equal(fixture.root.querySelector('[data-payment-retry]').hidden, true);
}
}

for (const [reference, verifyPath] of [
Expand Down Expand Up @@ -506,3 +509,17 @@ assert.ok(!config.includes('WORKERS_CI_BRANCH') && !config.includes('full-test-e
assert.ok(!page.includes('PUBLIC_DEVCON26_COMPLETE_TEST') && !page.includes('data-complete-test'));
assert.ok(page.includes('data-payment-mode-notice') && page.includes('data-checkout-challenge'));
console.log('PASS managed checkout: runtime test/live, stale-settings review without resubmission, bounded retries, fail-closed catalog/redirects, historical callbacks, and actual private email links.');

for (const mode of ['test', 'live']) {
const fixture = confirmationFixture(initialized(mode).reference, mode, { missingGrant: true });

await tick();
assert.equal(fixture.requests.length, 0);
assert.equal(fixture.root.querySelector('[data-payment-retry]').hidden, true);
assert.equal(fixture.root.dataset.paymentState, 'access_lost');
assert.equal(fixture.root.querySelector('[data-checkout-recovery]').hidden, false);
assert.equal(fixture.root.querySelector('[data-payment-receipt]').hidden, true);
assert.equal(fixture.root.querySelector('[data-recovery-reference]').textContent, fixture.exports.devcon26BuyerReference(initialized(mode).reference));
assert.ok(fixture.root.querySelector('[data-recovery-contact]').attributes.get('href').startsWith('mailto:hello@devcongress.org?subject='));
}
console.log('PASS lost-session recovery: no unauthenticated request or useless Retry in either mode.');
50 changes: 49 additions & 1 deletion src/components/HeroSection.astro
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface Props {
youtubeEmbedUrl: string;
}
const { hero, slackUrl, donationPath, youtubeEmbedUrl } = Astro.props;
const highlightGrowth = /\bgrow\.$/.test(hero.headline);
const headlinePrefix = highlightGrowth ? hero.headline.slice(0, -5) : hero.headline;

const colorClass: Record<string, string> = {
pink: 'btn-primary',
Expand All @@ -34,7 +36,7 @@ const colorClass: Record<string, string> = {
<div class="hero-top">
<div class="hero-copy">
<span class="eyebrow">{hero.eyebrow}</span>
<h1 class="heading-display hero-headline">{hero.headline}</h1>
<h1 class="heading-display hero-headline">{headlinePrefix}{highlightGrowth && <span class="growth-accent" data-growth-accent>grow.<svg viewBox="0 0 160 16" fill="none" aria-hidden="true" focusable="false"><path d="M5 8 Q76 1 155 10" stroke="currentColor" stroke-width="4" stroke-linecap="round" /></svg></span>}</h1>
<p class="hero-invitation">We connect all year. This December, we come together at <strong>DevCon26.</strong></p>
<div class="hero-conference-meta">
<time datetime="2026-12-19">19 December 2026</time>
Expand Down Expand Up @@ -95,6 +97,24 @@ const colorClass: Record<string, string> = {
</div>
</section>

<script>
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;

entry.target.classList.add('growth-accent--visible');
observer.unobserve(entry.target);
}
}, { threshold: 0.5 });

document.querySelectorAll('[data-growth-accent]').forEach((element) => {
element.classList.add('growth-accent--ready');
observer.observe(element);
});
}
</script>

<style>
.hero {
padding-top: clamp(3rem, 6vw, 5rem);
Expand All @@ -121,6 +141,28 @@ const colorClass: Record<string, string> = {
text-wrap: balance;
}

.growth-accent {
position: relative;
display: inline-block;
color: var(--pink);
white-space: nowrap;
}

.growth-accent svg {
position: absolute;
inset: auto 0 -0.14em;
width: 100%;
height: 0.18em;
pointer-events: none;
transform-origin: left center;
transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1), opacity 280ms cubic-bezier(0.16, 1, 0.3, 1);
}

.growth-accent--ready:not(.growth-accent--visible) svg {
transform: scaleX(0);
opacity: 0;
}

.hero-invitation {
max-width: 38ch;
font-size: var(--text-xl);
Expand Down Expand Up @@ -291,6 +333,12 @@ const colorClass: Record<string, string> = {
}

@media (prefers-reduced-motion: reduce) {
.growth-accent svg {
transition: none;
transform: none !important;
opacity: 1 !important;
}

.btn-hero, .btn-hero > span {
transition: none;
transform: none !important;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/devcon26-flexible-checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ export function initializeFlexibleCheckout(page: HTMLElement): void {
checkoutLayout.hidden = false;
status.textContent = settingsReviewRequired
? 'Checkout settings changed. Review your tickets and payment mode before continuing.'
: 'Checkout is open. Review your choices before continuing.';
: 'Checkout is open. Your seat is held for 15 minutes once checkout starts. Late payment confirmation may require a refund.';
renderRows();

if (settingsReviewRequired) {
Expand Down
23 changes: 21 additions & 2 deletions src/lib/devcon26-payment-confirmation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const TICKET_PRESENTATION = {
} as const;

type PaymentStatus = 'verified' | 'pending' | 'failed' | 'refund_required';
type PageState = PaymentStatus | 'loading' | 'invalid' | 'error';
type PageState = PaymentStatus | 'loading' | 'invalid' | 'error' | 'access_lost';

export interface TestPaymentSummary {
status: PaymentStatus;
Expand Down Expand Up @@ -215,6 +215,9 @@ export function initializeDevcon26PaymentConfirmation(
const status = root.querySelector<HTMLElement>('[data-payment-status]')!;
const receipt = root.querySelector<HTMLElement>('[data-payment-receipt]')!;
const modeNotice = root.querySelector<HTMLElement>('[data-payment-mode-notice]');
const recovery = root.querySelector<HTMLElement>('[data-checkout-recovery]');
const recoveryReference = root.querySelector<HTMLElement>('[data-recovery-reference]');
const recoveryContact = root.querySelector<HTMLAnchorElement>('[data-recovery-contact]');
const attention = root.querySelector<HTMLElement>('[data-payment-attention]')!;
const retry = root.querySelector<HTMLButtonElement>('[data-payment-retry]')!;
const providerReceipt = root.querySelector<HTMLAnchorElement>('[data-provider-receipt]')!;
Expand All @@ -233,6 +236,13 @@ export function initializeDevcon26PaymentConfirmation(
status.setAttribute('aria-busy', String(state === 'loading'));
receipt.hidden = state !== 'verified';
attention.hidden = state !== 'refund_required';
if (recovery) recovery.hidden = state !== 'access_lost';

if (state !== 'access_lost') {
if (recoveryReference) recoveryReference.textContent = '';
recoveryContact?.setAttribute('href', 'mailto:hello@devcongress.org?subject=DevCon26%20payment');
}

retry.hidden = !canRetry;
retry.disabled = state === 'loading';
document.title = heading + ' | DevCon26';
Expand Down Expand Up @@ -305,7 +315,16 @@ export function initializeDevcon26PaymentConfirmation(
}
}

if (!buyerGrant && !flexibleGrant) throw new Error('Secure checkout access is unavailable in this browser session.');
if (!buyerGrant && !flexibleGrant) {
showState('access_lost', 'Can’t access your checkout', 'This browser can’t check your payment. Open the link in your ticket email, or contact us with the reference below.');
const buyerReference = devcon26BuyerReference(reference);

if (recoveryReference) recoveryReference.textContent = buyerReference;
recoveryContact?.setAttribute('href', 'mailto:hello@devcongress.org?subject='
+ encodeURIComponent('DevCon26 payment — ' + buyerReference));

return;
}
}

const verifyPath = managedCheckout ? MANAGED_VERIFY_PATH
Expand Down
39 changes: 34 additions & 5 deletions src/pages/devcon26/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,9 @@ function packageEnquiryLink(sponsorship: (typeof sponsorshipPackages)[number]) {
<span class="event-year">2026</span>
</span>
</p>
<h1 id="event-title">A year together.<br /><em>One big gathering.</em></h1>
<p class="hero-summary">Meetups, ideas, and new connections all year. Join the community at DevCon26.</p>
<h1 id="event-title">Technology for the <em>Next Generation</em></h1>
<p class="hero-tagline">Innovation • Community • Impact</p>
<p class="hero-summary">With a gathering of builders, researchers, designers, security professionals, product managers and marketers. Come gain knowledge and exchange knowledge at DevCon26</p>
<div class="hero-actions">
<a class="button button-primary" href="#tickets">Buy tickets <span aria-hidden="true">↗</span></a>
<p>19 December 2026<br /><strong>Ghana Digital Center (formerly Accra Digital Center), Accra</strong></p>
Expand Down Expand Up @@ -684,6 +685,15 @@ function packageEnquiryLink(sponsorship: (typeof sponsorshipPackages)[number]) {
letter-spacing: -0.03em;
}

.hero-tagline {
max-width: 660px;
margin-top: 18px;
text-align: center;
font-size: var(--dc-text-body);
font-weight: 600;
line-height: 1.5;
}

.hero-summary {
max-width: 28rem;
margin-top: 22px;
Expand Down Expand Up @@ -2121,19 +2131,38 @@ function packageEnquiryLink(sponsorship: (typeof sponsorshipPackages)[number]) {
}

.hero h1 {
font-size: clamp(30px, 8.8vw, 42px);
letter-spacing: -0.025em;
text-align: center;
}

.hero h1 em {
display: block;
}

.hero-tagline {
margin-top: 12px;
font-size: 15px;
line-height: 1.4;
}

.hero-summary {
margin-top: 22px;
max-width: none;
margin-top: 20px;
font-size: 16px;
line-height: 1.55;
}

.hero-actions {
gap: 18px;
margin-top: 24px;
flex-direction: column;
text-align: center;
gap: 16px;
margin-top: 20px;
}

.hero-actions p {
max-width: 32ch;
text-wrap: balance;
font-size: 14px;
letter-spacing: 0;
}
Expand Down
Loading
Loading