WIP PWA for Grain
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());