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
62 changes: 62 additions & 0 deletions frontend/src/embed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Embed Module - Shows a banner when the game is loaded inside an iframe
* Promotes wordle.global to users playing via third-party iframe embeds
*/

import { isStandalone } from './pwa';

const DISMISS_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days

export const isEmbedded: boolean = (() => {
try {
return window.top !== window.self;
} catch {
// Cross-origin iframe throws SecurityError — that means we're embedded
return true;
}
})();

const isDismissed = (): boolean => {
try {
const dismissedAt = localStorage.getItem('embed_banner_dismissed_at');
if (!dismissedAt) return false;
const elapsed = Date.now() - parseInt(dismissedAt, 10);
return elapsed < DISMISS_DURATION_MS;
} catch {
// localStorage may throw in private browsing mode
return false;
}
};

const getBanner = (): HTMLElement | null => document.getElementById('embed-banner');

export const hideBanner = (): void => {
const banner = getBanner();
if (banner) banner.style.display = 'none';
};

export const showBanner = (): void => {
if (!isEmbedded || isDismissed() || isStandalone()) return;
const banner = getBanner();
if (banner) {
banner.style.display = 'flex';
}
};

export const dismiss = (): void => {
try {
localStorage.setItem('embed_banner_dismissed_at', Date.now().toString());
} catch {
// localStorage may throw in private browsing mode
}
hideBanner();
};

const embed = {
isEmbedded,
showBanner,
hideBanner,
dismiss,
};

export default embed;
12 changes: 10 additions & 2 deletions frontend/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
import { createApp } from 'vue';
import pwa from './pwa';
import embed from './embed';
import { haptic, setHapticsEnabled } from './haptics';
import { sound, setSoundEnabled } from './sounds';
import { buildNormalizeMap, buildNormalizedWordMap, normalizeWord } from './diacritics';
Expand Down Expand Up @@ -741,8 +742,12 @@ export const createGameApp = () => {
});
analytics.trackStreakMilestone(langCode, this.stats.current_streak);

// Show PWA install prompt after game completion
setTimeout(() => pwa.showBanner(), 2000);
// Show embed banner or PWA install prompt after game completion
// Each no-ops if not applicable (embed checks iframe, pwa checks standalone/prompt)
setTimeout(() => {
embed.showBanner();
pwa.showBanner();
}, 2000);
},

gameLost(): void {
Expand Down Expand Up @@ -783,6 +788,9 @@ export const createGameApp = () => {
had_frustration: lossFrustrationState.hadFrustration,
time_to_complete_seconds: lossTimeToComplete,
});

// Show embed banner after game loss (no-ops if not in iframe)
setTimeout(() => embed.showBanner(), 3000);
},

saveResult(won: boolean): void {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
import './style.css';
import pwa from './pwa';
import embed from './embed';
import './debug';
import createGameApp from './game';
import createIndexApp from './index-app';
Expand All @@ -14,6 +15,7 @@ declare global {
triggerPwaInstall: () => void;
dismissPwaInstall: () => void;
showPwaInstallBanner: () => void;
dismissEmbedBanner: () => void;
}
}

Expand All @@ -24,6 +26,7 @@ pwa.init();
window.triggerPwaInstall = pwa.install;
window.dismissPwaInstall = pwa.dismiss;
window.showPwaInstallBanner = pwa.showBanner;
window.dismissEmbedBanner = embed.dismiss;

// Detect which page we're on and create appropriate Vue app
const appEl = document.getElementById('app');
Expand Down
31 changes: 24 additions & 7 deletions frontend/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,12 @@
}
}

/* PWA Install Banner */
.pwa-install-banner {
/* Bottom Banners (shared base for PWA install + embed banners) */
.bottom-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 16px;
display: flex;
Expand All @@ -96,27 +95,45 @@
font-family: system-ui, -apple-system, sans-serif;
}

.pwa-install-banner button {
.bottom-banner .action-btn {
background: white;
color: #667eea;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
transition: transform 0.1s;
}

.pwa-install-banner button:active {
.bottom-banner .action-btn:active {
transform: scale(0.95);
}

.pwa-install-banner button.dismiss {
.bottom-banner button.dismiss {
background: transparent;
color: white;
border: none;
padding: 8px;
font-size: 20px;
line-height: 1;
cursor: pointer;
}

.pwa-install-banner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.pwa-install-banner .action-btn {
color: #667eea;
}

.embed-banner {
background: linear-gradient(135deg, #2d6a4f 0%, #1b4332 100%);
}

.embed-banner .action-btn {
color: #2d6a4f;
}

/* Prevent text selection on game elements */
Expand Down
15 changes: 13 additions & 2 deletions webapp/templates/game.html
Original file line number Diff line number Diff line change
Expand Up @@ -658,15 +658,26 @@ <h4 class="text-xs font-semibold uppercase tracking-wide mb-1 text-center text-n
<!-- /app -->

<!-- Simple banner to trigger the install dialog -->
<div id="pwa-install-banner" class="pwa-install-banner" style="display: none;">
<div id="pwa-install-banner" class="bottom-banner pwa-install-banner" style="display: none;">
<div style="flex: 1;">
<strong>{{ language.config.ui.add_to_home or "Add to Home Screen" }}</strong>
<div style="font-size: 13px; opacity: 0.9;">{{ language.config.ui.play_daily_like_app or "Play Wordle daily like an app" }}</div>
</div>
<button onclick="triggerPwaInstall()">{{ language.config.ui.install or "Install" }}</button>
<button class="action-btn" onclick="triggerPwaInstall()">{{ language.config.ui.install or "Install" }}</button>
<button class="dismiss" onclick="dismissPwaInstall()">&times;</button>
</div>

<!-- Embed banner for iframe embeds -->
<div id="embed-banner" class="bottom-banner embed-banner" style="display: none;">
<div style="flex: 1;">
<strong>Play Wordle in 65+ languages</strong>
</div>
<a class="action-btn" href="https://wordle.global/{{ language.config.language_code }}" target="_blank" rel="noopener">
Visit
</a>
<button class="dismiss" onclick="dismissEmbedBanner()">&times;</button>
</div>
Comment on lines +670 to +679
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: language.code attribute does not exist — link will be broken.

The Language class defines self.language_code (see webapp/app.py line 647), not self.code. This will cause the "Visit" link to render incorrectly (empty or error).

Compare with line 2 of this template which correctly uses language.language_code, and lines 49/55/59/71/87 which use language.config.language_code.

🐛 Proposed fix
-        <a href="https://wordle.global/{{ language.code }}" target="_blank" rel="noopener"
+        <a href="https://wordle.global/{{ language.language_code }}" target="_blank" rel="noopener"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/templates/game.html` around lines 652 - 662, The anchor in the embed
banner uses a nonexistent attribute language.code causing a broken link; update
the href to use the correct attribute language.language_code (i.e., change "{{
language.code }}" to "{{ language.language_code }}") so the Visit link points to
the proper URL; ensure any other occurrences within the embed banner (e.g., text
or JS interactions referencing language.code) are also updated to
language.language_code and keep the existing rel/target attributes and
dismissEmbedBanner() button unchanged.


{% set pwa_description = "Play Wordle in " ~ language.config.name_native %}
{% include 'partials/_pwa_install.html' %}

Expand Down
Loading