Simple vanilia JS vite project with ATProto OAuth out of the box
1const appElement = document.getElementById('app')
2
3/**
4 * Shows the login form and sets a event listener for form submission to start the OAuth flow.
5 */
6function showLoginForm() {
7 appElement.innerHTML = `
8 <div class="container">
9 <h1>ATProto OAuth Playground</h1>
10 <form id="login-form">
11 <div class="form-group">
12 <label for="handle">ATProto Handle</label>
13 <input
14 type="text"
15 id="handle"
16 name="handle"
17 placeholder="jcsalterego.bsky.social"
18 required
19 />
20 <div id="error" class="error"></div>
21 </div>
22 <button type="submit">Sign In</button>
23 </form>
24 </div>
25 `
26
27 document.querySelector('#login-form').addEventListener('submit', async (e) => {
28 e.preventDefault()
29
30 const handle = document.querySelector('#handle').value.trim()
31 const errorEl = document.querySelector('#error')
32 errorEl.textContent = ''
33
34 try {
35 // This will redirect to the OAuth authorization page on the PDS
36 await window.oauthClient.signIn(handle)
37 } catch (error) {
38 console.error('Sign in error:', error)
39 errorEl.textContent = error.message || 'Failed to sign in. Please check your handle and try again.'
40 }
41 })
42}
43
44/**
45 * Demo component to show an authenticated request by fetching the user's notifications.
46 * @returns {Promise<string>}
47 */
48async function notificationsList(){
49
50 const notifications = await window.atpAgent.app.bsky.notification.listNotifications({
51 limit: 5
52 })
53
54 return notifications.data.notifications.map(notif => {
55 const reasonText = {
56 'like': 'liked your post',
57 'repost': 'reposted your post',
58 'follow': 'followed you',
59 'mention': 'mentioned you',
60 'reply': 'replied to your post',
61 'quote': 'quoted your post'
62 }[notif.reason] || notif.reason
63
64 return `
65 <div class="notification-item ${!notif.isRead ? 'unread' : ''}">
66 <img src="${notif.author.avatar || '/vite.svg'}" alt="${notif.author.displayName}" class="notification-avatar" />
67 <div class="notification-content">
68 <p class="notification-text">
69 <strong>${notif.author.displayName || notif.author.handle}</strong> ${reasonText}
70 </p>
71 <p class="notification-time">${new Date(notif.indexedAt).toLocaleString()}</p>
72 </div>
73 </div>
74 `
75 }).join('')
76
77}
78
79/**
80 * Shows the logged in page with the user's profile and notifications.
81 */
82async function showLoggedInPage(session) {
83 const profile = await window.atpAgent.getProfile({
84 actor: session.sub
85 })
86
87 const { avatar, displayName, handle, followersCount, followsCount } = profile.data
88
89
90 appElement.innerHTML = `
91 <div class="container">
92 <h1>Logged In</h1>
93 <div class="profile-card">
94 <img src="${avatar || '/vite.svg'}" alt="Profile picture" class="profile-avatar" />
95 <div class="profile-info">
96 <h2 class="profile-name">${displayName || handle}</h2>
97 <p class="profile-handle">@${handle}</p>
98 <div class="profile-stats">
99 <span><strong>${followersCount || 0}</strong> Followers</span>
100 <span><strong>${followsCount || 0}</strong> Following</span>
101 </div>
102 </div>
103 </div>
104 <div class="notifications-section">
105 <h3>Recent Notifications</h3>
106 <div class="notifications-list">
107 ${await notificationsList()}
108 </div>
109 </div>
110 <button id="logout">Sign Out</button>
111 </div>
112 `
113
114 document.querySelector('#logout').addEventListener('click', async () => {
115 try {
116 await window.oauthClient.revoke(session.sub)
117 showLoginForm()
118 } catch (error) {
119 console.error('Sign out error:', error)
120 }
121 })
122}
123
124/**
125 *
126 * DANGER WILL ROBINSON
127 */
128function showError(message) {
129 appElement.innerHTML = `
130 <div class="container">
131 <h1>ATProto OAuth Playground</h1>
132 <div class="error">${message}</div>
133 <a href="/">Back to login</a>
134 </div>
135 `
136}
137
138
139export { showLoginForm, showLoggedInPage, showError }