Skip to content

Commit ece2a47

Browse files
committed
Support iOS Google auth and notifications
1 parent 5148ee4 commit ece2a47

3 files changed

Lines changed: 180 additions & 12 deletions

File tree

app/account/page.tsx

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { useRouter } from "next/navigation";
66
import {
77
createUserWithEmailAndPassword,
88
deleteUser,
9+
getRedirectResult,
910
getAdditionalUserInfo,
1011
GoogleAuthProvider,
1112
sendPasswordResetEmail,
1213
signInWithEmailAndPassword,
1314
signInWithPopup,
15+
signInWithRedirect,
1416
signOut,
1517
updateProfile,
1618
} from "firebase/auth";
@@ -48,6 +50,15 @@ const fieldClassName =
4850
const primaryButtonClassName =
4951
"group inline-flex min-h-16 w-full items-center justify-between bg-white px-5 font-semibold uppercase text-black transition-colors duration-300 hover:bg-[#8a2ae3] hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#8a2ae3] active:translate-y-px disabled:cursor-not-allowed disabled:opacity-40";
5052
const CMS_PROFILE_URL = "https://cms.lap.onl/admin/profile";
53+
const GOOGLE_REDIRECT_MODE_KEY = "lap_google_redirect_mode";
54+
55+
function shouldUseGoogleRedirect() {
56+
if (typeof navigator === "undefined") return false;
57+
return (
58+
/iPad|iPhone|iPod/i.test(navigator.userAgent) ||
59+
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)
60+
);
61+
}
5162

5263
async function checkCurrentDevice() {
5364
const payload = await getDeviceRiskPayload();
@@ -179,6 +190,83 @@ export default function AccountPage() {
179190
setHandle(profile?.handle || "");
180191
}, [profile?.handle]);
181192

193+
useEffect(() => {
194+
let cancelled = false;
195+
196+
const completeGoogleRedirect = async () => {
197+
const pendingMode = window.sessionStorage.getItem(GOOGLE_REDIRECT_MODE_KEY);
198+
199+
try {
200+
const credential = await getRedirectResult(auth);
201+
if (!credential || cancelled) return;
202+
203+
window.sessionStorage.removeItem(GOOGLE_REDIRECT_MODE_KEY);
204+
setBusy(true);
205+
setError("");
206+
setMessage("");
207+
208+
const redirectMode = pendingMode === "register" ? "register" : "signin";
209+
const isNewFirebaseUser = getAdditionalUserInfo(credential)?.isNewUser === true;
210+
211+
if (redirectMode === "signin" && isNewFirebaseUser) {
212+
await deleteUser(credential.user);
213+
if (!cancelled) {
214+
setMode("register");
215+
setError("No account was found. Create an account with Google below.");
216+
}
217+
return;
218+
}
219+
220+
const syncedRisk = await syncCurrentDevice();
221+
if (syncedRisk.blocked) {
222+
if (isNewFirebaseUser) {
223+
await deleteUser(credential.user).catch(() => undefined);
224+
}
225+
await signOut(auth).catch(() => undefined);
226+
if (!cancelled) {
227+
setError(
228+
syncedRisk.reason ||
229+
"This browser installation has been blocked due to Community Guidelines violations.",
230+
);
231+
}
232+
return;
233+
}
234+
235+
const existingProfile = await getExistingPublicProfile(credential.user);
236+
if (existingProfile) {
237+
await syncPublicUser(credential.user);
238+
await refreshProfile();
239+
}
240+
241+
if (!cancelled) {
242+
if (redirectMode === "register") {
243+
setMessage(
244+
existingProfile?.handle
245+
? `Your account already exists as @${existingProfile.handle}.`
246+
: "Account created. Add your photo and handle to finish.",
247+
);
248+
} else {
249+
setMessage(
250+
existingProfile
251+
? "Signed in."
252+
: "Welcome back. Finish your photo and handle to continue.",
253+
);
254+
}
255+
}
256+
} catch (nextError) {
257+
window.sessionStorage.removeItem(GOOGLE_REDIRECT_MODE_KEY);
258+
if (!cancelled) setError(friendlyAuthError(nextError));
259+
} finally {
260+
if (!cancelled) setBusy(false);
261+
}
262+
};
263+
264+
void completeGoogleRedirect();
265+
return () => {
266+
cancelled = true;
267+
};
268+
}, [refreshProfile]);
269+
182270
useEffect(() => {
183271
if (!isStaff || profile?.handle) return;
184272
setMessage((current) =>
@@ -322,7 +410,14 @@ export default function AccountPage() {
322410
return;
323411
}
324412

325-
const credential = await signInWithPopup(auth, new GoogleAuthProvider());
413+
const provider = new GoogleAuthProvider();
414+
if (shouldUseGoogleRedirect()) {
415+
window.sessionStorage.setItem(GOOGLE_REDIRECT_MODE_KEY, mode);
416+
await signInWithRedirect(auth, provider);
417+
return;
418+
}
419+
420+
const credential = await signInWithPopup(auth, provider);
326421
const isNewFirebaseUser = getAdditionalUserInfo(credential)?.isNewUser === true;
327422

328423
if (mode === "signin" && isNewFirebaseUser) {

components/NotificationBell.tsx

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,29 @@ export type NotificationItem = {
5151
};
5252
};
5353

54+
type BrowserNotificationSupport =
55+
| "checking"
56+
| "available"
57+
| "ios-browser"
58+
| "unsupported";
59+
60+
function isAppleMobileBrowser() {
61+
if (typeof navigator === "undefined") return false;
62+
return (
63+
/iPad|iPhone|iPod/i.test(navigator.userAgent) ||
64+
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)
65+
);
66+
}
67+
68+
function isStandaloneWebApp() {
69+
if (typeof window === "undefined") return false;
70+
const standaloneNavigator = navigator as Navigator & { standalone?: boolean };
71+
return (
72+
window.matchMedia("(display-mode: standalone)").matches ||
73+
standaloneNavigator.standalone === true
74+
);
75+
}
76+
5477
function formatTimeAgo(timestamp?: Timestamp): string {
5578
if (!timestamp) return "Just now";
5679
const seconds = Math.floor((Date.now() - timestamp.toMillis()) / 1000);
@@ -80,12 +103,19 @@ export default function NotificationBell({
80103
const [filter, setFilter] = useState<"all" | "unread">("all");
81104
const popoverRef = useRef<HTMLDivElement>(null);
82105
const [browserPermission, setBrowserPermission] = useState<NotificationPermission>("default");
106+
const [browserSupport, setBrowserSupport] =
107+
useState<BrowserNotificationSupport>("checking");
83108
const isInitialSnapshotRef = useRef(true);
84109

85110
// Check browser notification permission
86111
useEffect(() => {
87112
if (typeof window !== "undefined" && "Notification" in window) {
88113
setBrowserPermission(Notification.permission);
114+
setBrowserSupport("available");
115+
} else if (isAppleMobileBrowser() && !isStandaloneWebApp()) {
116+
setBrowserSupport("ios-browser");
117+
} else {
118+
setBrowserSupport("unsupported");
89119
}
90120
}, []);
91121

@@ -141,18 +171,37 @@ export default function NotificationBell({
141171
Notification.permission === "granted"
142172
) {
143173
try {
144-
const popup = new Notification(data.title || "New Notification", {
174+
const options: NotificationOptions = {
145175
body: data.message || "You received a new notification on L.A.P Tutorials.",
146-
icon: "/favicon.ico",
176+
icon: "/icons/android-chrome-192x192.png",
147177
tag: change.doc.id,
148-
});
149-
popup.onclick = () => {
150-
window.focus();
151-
if (data.link) {
152-
router.push(data.link);
153-
}
154-
popup.close();
178+
data: { url: data.link || "/" },
155179
};
180+
181+
if ("serviceWorker" in navigator) {
182+
void navigator.serviceWorker.ready
183+
.then((registration) =>
184+
registration.showNotification(
185+
data.title || "New Notification",
186+
options,
187+
),
188+
)
189+
.catch((popupErr) => {
190+
console.error("Error displaying service worker notification:", popupErr);
191+
});
192+
} else {
193+
const popup = new Notification(
194+
data.title || "New Notification",
195+
options,
196+
);
197+
popup.onclick = () => {
198+
window.focus();
199+
if (data.link) {
200+
router.push(data.link);
201+
}
202+
popup.close();
203+
};
204+
}
156205
} catch (popupErr) {
157206
console.error("Error displaying native notification:", popupErr);
158207
}
@@ -375,20 +424,25 @@ export default function NotificationBell({
375424
</div>
376425

377426
{/* Browser notification prompt banner */}
378-
{browserPermission === "default" && (
427+
{browserSupport === "available" && browserPermission === "default" && (
379428
<button
380429
type="button"
381430
onClick={requestBrowserPermission}
382431
className="flex w-full items-center justify-between border-b border-white/10 bg-[#8a2ae3]/10 px-4 py-2 text-left text-xs text-[#8a2ae3] transition-colors hover:bg-[#8a2ae3]/20"
383432
>
384433
<span className="flex items-center gap-1.5 font-medium">
385-
<Bell className="h-3.5 w-3.5" /> Enable browser pop-ups
434+
<Bell className="h-3.5 w-3.5" /> Enable notifications
386435
</span>
387436
<span className="font-mono text-[10px] font-bold uppercase underline">
388437
Enable
389438
</span>
390439
</button>
391440
)}
441+
{browserSupport === "ios-browser" && (
442+
<div className="border-b border-white/10 bg-[#8a2ae3]/10 px-4 py-2 text-xs leading-relaxed text-[#c997ff]">
443+
Add L.A.P Docs to your iPad Home Screen, open it there, then enable notifications.
444+
</div>
445+
)}
392446

393447
{/* Filter Tabs */}
394448
<div className="flex border-b border-white/10 text-xs font-mono">

public/sw.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,25 @@ self.addEventListener('activate', (event) => {
3232
self.clients.claim();
3333
});
3434

35+
self.addEventListener('notificationclick', (event) => {
36+
event.notification.close();
37+
38+
const requestedUrl = event.notification.data?.url || '/';
39+
const destination = new URL(requestedUrl, self.location.origin);
40+
const safeUrl = destination.origin === self.location.origin ? destination.href : self.location.origin;
41+
42+
event.waitUntil(
43+
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windows) => {
44+
for (const client of windows) {
45+
if ('navigate' in client && 'focus' in client) {
46+
return client.navigate(safeUrl).then(() => client.focus());
47+
}
48+
}
49+
return self.clients.openWindow(safeUrl);
50+
})
51+
);
52+
});
53+
3554
self.addEventListener('fetch', (event) => {
3655
if (event.request.method !== 'GET') return;
3756

0 commit comments

Comments
 (0)