WIP PWA for Grain
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 121 lines 3.2 kB view raw
1import { createQuicksliceClient } from 'quickslice-client-js'; 2import { router } from '../router.js'; 3import { grainApi } from './grain-api.js'; 4 5class AuthService { 6 #client = null; 7 #user = null; 8 #listeners = new Set(); 9 #initialized = false; 10 11 async init() { 12 if (this.#initialized) return; 13 14 this.#client = await createQuicksliceClient({ 15 server: 'https://quickslice-production-9cf4.up.railway.app', 16 clientId: 'client_h62Ea0FUeXTJ4pWBg4ZIkQ', 17 scope: 'atproto blob:image/* repo:social.grain.gallery repo:social.grain.gallery.item repo:social.grain.photo repo:social.grain.favorite repo:social.grain.graph.follow repo:social.grain.actor.profile repo:social.grain.comment' 18 }); 19 20 // Handle OAuth callback if present 21 if (window.location.search.includes('code=')) { 22 await this.#client.handleRedirectCallback(); 23 24 // Check if user has a Grain profile 25 const hasProfile = await grainApi.hasGrainProfile(this.#client); 26 27 if (!hasProfile) { 28 // First-time user - redirect to onboarding 29 window.location.replace('/onboarding'); 30 return; 31 } 32 33 // Existing user - redirect to their destination 34 const returnUrl = sessionStorage.getItem('oauth_return_url') || '/'; 35 sessionStorage.removeItem('oauth_return_url'); 36 window.location.replace(returnUrl); 37 return; 38 } 39 40 // Load user if authenticated 41 if (await this.#client.isAuthenticated()) { 42 await this.#loadUser(); 43 } 44 45 this.#initialized = true; 46 } 47 48 async #loadUser() { 49 const clientUser = this.#client.getUser(); 50 const did = clientUser?.did || clientUser?.sub; 51 const result = await this.#client.query(` 52 query { 53 viewer { 54 did 55 handle 56 socialGrainActorProfileByDid { 57 displayName 58 avatar { url(preset: "avatar") } 59 } 60 } 61 } 62 `); 63 const viewer = result.viewer; 64 const grainProfile = viewer?.socialGrainActorProfileByDid; 65 this.#user = { 66 did: did || viewer?.did, 67 handle: viewer?.handle, 68 displayName: grainProfile?.displayName || '', 69 avatar: grainProfile?.avatar || null 70 }; 71 this.#notify(); 72 } 73 74 async login(handle) { 75 sessionStorage.setItem('oauth_return_url', window.location.pathname); 76 sessionStorage.setItem('oauth_handle', handle); 77 window.location.href = '/oauth/callback?start=1'; 78 } 79 80 async startOAuthFromCallback() { 81 const handle = sessionStorage.getItem('oauth_handle'); 82 sessionStorage.removeItem('oauth_handle'); 83 if (!handle) { 84 router.replace('/'); 85 return; 86 } 87 await this.#client.loginWithRedirect({ handle }); 88 } 89 90 logout() { 91 this.#client.logout(); 92 } 93 94 get user() { 95 return this.#user; 96 } 97 98 get isAuthenticated() { 99 return !!this.#user; 100 } 101 102 subscribe(callback) { 103 this.#listeners.add(callback); 104 return () => this.#listeners.delete(callback); 105 } 106 107 #notify() { 108 this.#listeners.forEach(cb => cb(this.#user)); 109 } 110 111 getClient() { 112 return this.#client; 113 } 114 115 async refreshUser() { 116 await this.#loadUser(); 117 } 118} 119 120// Preserve auth instance across HMR 121export const auth = window.__auth || (window.__auth = new AuthService());