Skip to content

Commit fa20ad5

Browse files
ClaudeMMclaude
andcommitted
Fix prereg: direct Supabase auth + DB insert, remove xfor.bot redirect
- Google OAuth handled client-side via Supabase JS - Preregistrations saved directly to preregistrations table via upsert - No server-side middleman needed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c2700b7 commit fa20ad5

File tree

1 file changed

+128
-112
lines changed

1 file changed

+128
-112
lines changed

docs/app.js

Lines changed: 128 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,48 @@
1-
const preregisterButton = document.getElementById('google-preregister-button');
2-
const signOutButton = document.getElementById('google-signout-button');
3-
const statusText = document.getElementById('preregister-status');
4-
const resultBanner = document.getElementById('preregister-result');
5-
const preregisterModal = document.getElementById('preregister-modal');
6-
const preregisterModalClose = document.getElementById('preregister-modal-close');
1+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
2+
3+
const SUPABASE_URL = "https://kvezyhwbkvpyndkaemsw.supabase.co";
4+
const SUPABASE_ANON_KEY = "sb_publishable_MWtbxI1_Gm_yYTSxHofq2Q_z0nGwItH";
5+
6+
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
7+
auth: {
8+
autoRefreshToken: true,
9+
detectSessionInUrl: true,
10+
persistSession: true,
11+
flowType: "pkce",
12+
},
13+
});
14+
15+
const preregisterButton = document.getElementById("google-preregister-button");
16+
const signOutButton = document.getElementById("google-signout-button");
17+
const statusText = document.getElementById("preregister-status");
18+
const resultBanner = document.getElementById("preregister-result");
19+
const preregisterModal = document.getElementById("preregister-modal");
20+
const preregisterModalClose = document.getElementById("preregister-modal-close");
721
const watchModelInputs = Array.from(document.querySelectorAll('input[name="watch-model"]'));
8-
const feedbackInput = document.getElementById('preregister-feedback');
9-
10-
const PREREG_DRAFT_KEY = 'clawwatch-preregister-draft';
11-
const PREREG_PENDING_KEY = 'clawwatch-preregister-pending';
12-
const XFOR_PREREGISTER_START_URL = 'https://xfor.bot/api/v1/clawwatch/start';
22+
const feedbackInput = document.getElementById("preregister-feedback");
1323

1424
let hasShownThankYou = false;
1525

1626
function showThankYouModal() {
1727
if (!preregisterModal || hasShownThankYou) return;
1828
hasShownThankYou = true;
1929
preregisterModal.hidden = false;
20-
document.body.style.overflow = 'hidden';
30+
document.body.style.overflow = "hidden";
2131
}
2232

2333
function closeThankYouModal() {
2434
if (!preregisterModal) return;
2535
preregisterModal.hidden = true;
26-
document.body.style.overflow = '';
36+
document.body.style.overflow = "";
2737
}
2838

2939
function track(eventName, params = {}) {
30-
if (typeof window.gtag === 'function') {
31-
window.gtag('event', eventName, params);
40+
if (typeof window.gtag === "function") {
41+
window.gtag("event", eventName, params);
3242
}
3343
}
3444

35-
function setBanner(message, tone = 'info') {
45+
function setBanner(message, tone = "info") {
3646
if (!resultBanner) return;
3747
resultBanner.hidden = false;
3848
resultBanner.dataset.tone = tone;
@@ -42,144 +52,150 @@ function setBanner(message, tone = 'info') {
4252
function clearBanner() {
4353
if (!resultBanner) return;
4454
resultBanner.hidden = true;
45-
resultBanner.textContent = '';
55+
resultBanner.textContent = "";
4656
delete resultBanner.dataset.tone;
4757
}
4858

4959
function getFormState() {
5060
return {
51-
watchModels: watchModelInputs.filter((input) => input.checked).map((input) => input.value),
52-
feedback: feedbackInput?.value.trim() || '',
61+
watchModels: watchModelInputs.filter((i) => i.checked).map((i) => i.value),
62+
feedback: feedbackInput?.value.trim() || "",
5363
};
5464
}
5565

56-
function saveDraftState() {
57-
try {
58-
window.localStorage.setItem(PREREG_DRAFT_KEY, JSON.stringify(getFormState()));
59-
} catch {}
60-
}
61-
62-
function loadDraftState() {
63-
try {
64-
const raw = window.localStorage.getItem(PREREG_DRAFT_KEY);
65-
if (!raw) return null;
66-
return JSON.parse(raw);
67-
} catch {
68-
return null;
69-
}
70-
}
71-
72-
function clearDraftState() {
73-
try {
74-
window.localStorage.removeItem(PREREG_DRAFT_KEY);
75-
} catch {}
76-
}
77-
78-
function setPendingPreregistration(isPending) {
79-
try {
80-
if (isPending) {
81-
window.localStorage.setItem(PREREG_PENDING_KEY, '1');
82-
} else {
83-
window.localStorage.removeItem(PREREG_PENDING_KEY);
84-
}
85-
} catch {}
86-
}
87-
88-
function applyDraftState(draft = {}) {
89-
const selected = Array.isArray(draft.watchModels) ? draft.watchModels : [];
90-
const selectedSet = new Set(selected);
91-
watchModelInputs.forEach((input) => {
92-
input.checked = selectedSet.has(input.value);
93-
});
94-
if (feedbackInput) {
95-
feedbackInput.value = typeof draft.feedback === 'string' ? draft.feedback : '';
96-
}
66+
function applyFormState(watchModels = [], feedback = "") {
67+
const selected = new Set(watchModels);
68+
watchModelInputs.forEach((i) => { i.checked = selected.has(i.value); });
69+
if (feedbackInput) feedbackInput.value = feedback;
9770
}
9871

9972
function renderReady() {
10073
clearBanner();
101-
if (statusText) {
102-
statusText.textContent = 'Sign in with Google and we will mark your account for the future install-ready ClawWatch release.';
103-
}
74+
if (statusText) statusText.textContent = "Sign in with Google and we will mark your account for the future install-ready ClawWatch release.";
10475
if (preregisterButton) {
10576
preregisterButton.disabled = false;
10677
preregisterButton.innerHTML = '<span class="google-mark">G</span><span>Continue with Google</span>';
10778
}
108-
if (signOutButton) {
109-
signOutButton.hidden = true;
110-
}
79+
if (signOutButton) signOutButton.hidden = true;
11180
}
11281

11382
function renderBusy(message) {
11483
clearBanner();
115-
if (statusText) {
116-
statusText.textContent = message;
117-
}
84+
if (statusText) statusText.textContent = message;
11885
if (preregisterButton) {
11986
preregisterButton.disabled = true;
120-
preregisterButton.innerHTML = '<span class="google-mark"></span><span>Working</span>';
87+
preregisterButton.innerHTML = '<span class="google-mark">\u2026</span><span>Working\u2026</span>';
12188
}
122-
if (signOutButton) {
123-
signOutButton.hidden = true;
89+
if (signOutButton) signOutButton.hidden = true;
90+
}
91+
92+
function renderRegistered(email) {
93+
if (statusText) statusText.textContent = "Registered as " + email + ". Update your details below any time.";
94+
if (preregisterButton) {
95+
preregisterButton.disabled = false;
96+
preregisterButton.innerHTML = '<span class="google-mark">\u2713</span><span>Update my details</span>';
12497
}
98+
if (signOutButton) signOutButton.hidden = false;
99+
setBanner("Thank you for your interest! We will notify you when the easy-install ClawWatch is ready.", "success");
125100
}
126101

127-
function buildSharedAuthRedirectUrl() {
102+
async function savePreregistration(user) {
128103
const { watchModels, feedback } = getFormState();
129-
const params = new URLSearchParams({
130-
return_to: `${window.location.origin}${window.location.pathname}?preregister=complete#preregister`,
131-
watch_models: watchModels.join(','),
132-
feedback,
104+
renderBusy("Saving your preregistration...");
105+
106+
const row = {
107+
user_id: user.id,
108+
email: user.email,
109+
watch_models: watchModels,
110+
has_watch: watchModels.length > 0 && !watchModels.includes("no-watch-yet"),
111+
feedback: feedback,
112+
source: window.location.host,
113+
registered_at: new Date().toISOString(),
114+
updated_at: new Date().toISOString(),
115+
};
116+
117+
const { error } = await supabase.from("preregistrations").upsert(row, { onConflict: "user_id" });
118+
119+
if (error) {
120+
console.error("preregistrations upsert failed:", error);
121+
setBanner("Sign-in worked but saving failed: " + error.message, "error");
122+
renderReady();
123+
return;
124+
}
125+
126+
track("clawwatch_preregister_complete");
127+
applyFormState(watchModels, feedback);
128+
renderRegistered(user.email || "your account");
129+
showThankYouModal();
130+
}
131+
132+
async function startGoogleSignIn() {
133+
clearBanner();
134+
renderBusy("Redirecting to Google sign-in...");
135+
track("clawwatch_preregister_start");
136+
137+
const redirectTo = window.location.origin + window.location.pathname + "?preregister=complete#preregister";
138+
const { data, error } = await supabase.auth.signInWithOAuth({
139+
provider: "google",
140+
options: { redirectTo: redirectTo, queryParams: { prompt: "select_account" } },
133141
});
134-
return `${XFOR_PREREGISTER_START_URL}?${params.toString()}`;
142+
143+
if (error) {
144+
renderReady();
145+
setBanner(error.message, "error");
146+
}
135147
}
136148

137-
function startGoogleSignIn() {
138-
renderBusy('Redirecting to Google sign-in…');
139-
track('clawwatch_preregister_start');
140-
saveDraftState();
141-
setPendingPreregistration(true);
142-
window.location.assign(buildSharedAuthRedirectUrl());
149+
async function signOut() {
150+
await supabase.auth.signOut();
151+
renderReady();
143152
}
144153

145-
function init() {
146-
preregisterButton?.addEventListener('click', startGoogleSignIn);
147-
preregisterModalClose?.addEventListener('click', closeThankYouModal);
148-
preregisterModal?.addEventListener('click', (event) => {
149-
if (event.target?.dataset?.closeModal === 'true') {
150-
closeThankYouModal();
154+
async function init() {
155+
preregisterButton?.addEventListener("click", async function() {
156+
const { data } = await supabase.auth.getSession();
157+
if (data.session?.user) {
158+
await savePreregistration(data.session.user);
159+
} else {
160+
await startGoogleSignIn();
151161
}
152162
});
163+
signOutButton?.addEventListener("click", signOut);
164+
preregisterModalClose?.addEventListener("click", closeThankYouModal);
165+
preregisterModal?.addEventListener("click", function(e) {
166+
if (e.target?.dataset?.closeModal === "true") closeThankYouModal();
167+
});
153168

154-
const savedDraft = loadDraftState();
155-
if (savedDraft) {
156-
applyDraftState(savedDraft);
157-
}
169+
const { data, error } = await supabase.auth.getSession();
170+
if (error) { setBanner(error.message, "error"); return; }
158171

159-
const url = new URL(window.location.href);
160-
const preregStatus = url.searchParams.get('preregister');
172+
const isReturning = new URLSearchParams(window.location.search).get("preregister") === "complete";
161173

162-
if (preregStatus === 'complete') {
163-
if (savedDraft) {
164-
applyDraftState(savedDraft);
174+
if (data.session?.user) {
175+
if (isReturning) {
176+
await savePreregistration(data.session.user);
177+
} else {
178+
renderRegistered(data.session.user.email || "your account");
165179
}
166-
clearDraftState();
167-
setPendingPreregistration(false);
168-
renderReady();
169-
setBanner("Thank you for your interest! We'll be back when the easy-to-install ClawWatch is here.", 'success');
170-
showThankYouModal();
171-
} else if (preregStatus === 'error') {
172-
renderReady();
173-
setPendingPreregistration(false);
174-
setBanner('Google sign-in finished, but ClawWatch preregistration was not stored. Please try again.', 'error');
175180
} else {
176181
renderReady();
177182
}
178183

179-
if (preregStatus) {
180-
const cleanUrl = `${window.location.origin}${window.location.pathname}${window.location.hash || ''}`;
181-
window.history.replaceState({}, document.title, cleanUrl);
184+
if (isReturning) {
185+
var clean = window.location.origin + window.location.pathname + (window.location.hash || "");
186+
window.history.replaceState({}, document.title, clean);
182187
}
188+
189+
supabase.auth.onAuthStateChange(async function(event, session) {
190+
if (event === "SIGNED_IN" && session?.user) {
191+
await savePreregistration(session.user);
192+
} else if (event === "SIGNED_OUT") {
193+
renderReady();
194+
}
195+
});
183196
}
184197

185-
init();
198+
init().catch(function(err) {
199+
renderReady();
200+
setBanner(err.message || "Failed to initialize.", "error");
201+
});

0 commit comments

Comments
 (0)