signinwith 1.0.12 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -52,12 +52,68 @@ export const verifySigninDiscord = async (config, verificationData) => {
52
52
  return profile.email ? { success: true, email: profile.email } : { success: false, error: 'Email not available from Discord' };
53
53
  };
54
54
 
55
+ export const verifySigninGithub = async (config, verificationData) => {
56
+ if (!verificationData?.code) return { success: false, error: 'Missing GitHub authorization code' };
57
+ if (!config?.clientId || !config?.clientSecret) return { success: false, error: 'GitHub clientId and clientSecret are required' };
58
+
59
+ const params = new URLSearchParams();
60
+ params.append('client_id', config.clientId);
61
+ params.append('client_secret', config.clientSecret);
62
+ params.append('code', verificationData.code);
63
+ if (config.redirectUri) params.append('redirect_uri', config.redirectUri);
64
+ if (verificationData.codeVerifier) params.append('code_verifier', verificationData.codeVerifier);
65
+
66
+ const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Accept': 'application/json',
70
+ },
71
+ body: params,
72
+ });
73
+ const token = await tokenResponse.json();
74
+ if (!tokenResponse.ok || token.error) {
75
+ return { success: false, error: token.error_description || 'Failed to exchange GitHub code for token' };
76
+ }
77
+
78
+ const accessToken = token.access_token;
79
+ const apiHeaders = {
80
+ 'Authorization': `Bearer ${accessToken}`,
81
+ 'Accept': 'application/vnd.github+json',
82
+ 'User-Agent': config.userAgent || 'signinwith',
83
+ };
84
+
85
+ const profileRes = await fetch('https://api.github.com/user', { headers: apiHeaders });
86
+ const profile = await profileRes.json();
87
+ if (!profileRes.ok) {
88
+ return { success: false, error: profile.message || 'Failed to fetch GitHub profile' };
89
+ }
90
+ if (profile.email) {
91
+ return { success: true, email: profile.email };
92
+ }
93
+
94
+ const emailRes = await fetch('https://api.github.com/user/emails', { headers: apiHeaders });
95
+ if (emailRes.ok) {
96
+ const emails = await emailRes.json();
97
+ if (Array.isArray(emails)) {
98
+ const primary = emails.find((item) => item.primary && item.verified);
99
+ const verified = emails.find((item) => item.verified);
100
+ const email = primary?.email || verified?.email;
101
+ if (email) {
102
+ return { success: true, email };
103
+ }
104
+ }
105
+ }
106
+
107
+ return { success: false, error: 'Email not available from GitHub' };
108
+ };
109
+
55
110
  export default async function verifySignin (services, service, verificationData) {
56
111
  try {
57
112
  if (services.google && service === 'google') return await verifySigninGoogle(services.google, verificationData);
58
113
  if (services.facebook && service === 'facebook') return await verifySigninFacebook(services.facebook, verificationData);
59
114
  if (services.apple && service === 'apple') return await verifySigninApple(services.apple, verificationData);
60
115
  if (services.discord && service === 'discord') return await verifySigninDiscord(services.discord, verificationData);
116
+ if (services.github && service === 'github') return await verifySigninGithub(services.github, verificationData);
61
117
  return { success: false, error: 'Unsupported service' };
62
118
  } catch (err) {
63
119
  return { success: false, error: err.message || 'Unknown error' };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "signinwith",
3
- "version": "1.0.12",
4
- "description": "Simple and straightforward library for sign in / sign up with thirdparty oAuth services like Google, Meta, Apple, Discord...",
3
+ "version": "1.1.0",
4
+ "description": "Simple and straightforward library for sign in / sign up with thirdparty oAuth services like Google, Facebook, Apple, Discord...",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  },
package/react.jsx CHANGED
@@ -17,6 +17,17 @@ const DiscordIcon = () => (
17
17
  <svg viewBox="0 -28.5 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z" fill="#FFFFFF"/></svg>
18
18
  );
19
19
 
20
+ const GithubIcon = () => (
21
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
22
+ <path
23
+ fill-rule="evenodd"
24
+ clip-rule="evenodd"
25
+ d="M12 0C5.37 0 0 5.42 0 12.108c0 5.354 3.438 9.892 8.205 11.496.6.115.82-.264.82-.585 0-.288-.01-1.049-.016-2.06-3.338.737-4.042-1.642-4.042-1.642-.547-1.412-1.336-1.788-1.336-1.788-1.09-.758.083-.743.083-.743 1.205.086 1.84 1.255 1.84 1.255 1.07 1.864 2.807 1.326 3.492 1.015.108-.793.42-1.326.763-1.63-2.665-.31-5.466-1.366-5.466-6.075 0-1.342.465-2.44 1.232-3.3-.124-.311-.535-1.565.118-3.263 0 0 1.007-.33 3.3 1.26a11.23 11.23 0 0 1 3.004-.41c1.02.004 2.047.14 3.004.41 2.29-1.59 3.296-1.26 3.296-1.26.655 1.698.244 2.952.12 3.263.77.86 1.23 1.958 1.23 3.3 0 4.724-2.807 5.76-5.48 6.064.43.379.823 1.126.823 2.27 0 1.64-.015 2.96-.015 3.363 0 .324.216.704.826.583C20.565 22 24 17.46 24 12.108 24 5.42 18.627 0 12 0Z"
26
+ fill="#FFFFFF"
27
+ />
28
+ </svg>
29
+ );
30
+
20
31
  // Subcomponent: Facebook
21
32
  export function SignInWithFacebook({ service, onSignin, onError }) {
22
33
  useEffect(() => {
@@ -205,11 +216,54 @@ export function SignInWithApple({ service, onSignin, onError }) {
205
216
 
206
217
  // Subcomponent: Discord
207
218
  export function SignInWithDiscord({ service, onSignin, onError }) {
219
+ const popupRef = useRef(null);
220
+
221
+ useEffect(() => {
222
+ if (typeof BroadcastChannel === 'undefined') {
223
+ console.warn('BroadcastChannel is not supported in this browser. Discord login will not work.');
224
+ return;
225
+ }
226
+
227
+ const channel = new BroadcastChannel('signinwith-discord');
228
+ const handleChannelMessage = (event) => {
229
+ const data = event.data;
230
+ if (!data || data.service !== 'discord') return;
231
+
232
+ if (data.code) {
233
+ onSignin('discord', { code: data.code, state: data.state });
234
+ } else if (data.error) {
235
+ onError?.(`Discord login error: ${data.error}`);
236
+ } else {
237
+ onError?.('Unknown Discord login error.');
238
+ }
239
+
240
+ if (popupRef.current && !popupRef.current.closed) {
241
+ popupRef.current.close();
242
+ }
243
+ window.__signinwithPendingProvider = null;
244
+ window.__signinwithPendingProviderState = null;
245
+ };
246
+
247
+ channel.addEventListener('message', handleChannelMessage);
248
+
249
+ return () => {
250
+ channel.removeEventListener('message', handleChannelMessage);
251
+ channel.close();
252
+ };
253
+ }, [onSignin, onError]);
254
+
208
255
  const handleDiscordLogin = (e) => {
209
256
  e.stopPropagation();
210
257
  e.preventDefault();
211
258
 
212
- const { clientId, redirectUri, scope = 'identify email' } = service; // Default scopes
259
+ const {
260
+ clientId,
261
+ redirectUri,
262
+ scope = 'identify email',
263
+ state = 'discord',
264
+ prompt = 'consent',
265
+ popupFeatures = 'width=500,height=700',
266
+ } = service; // Default scopes
213
267
 
214
268
  if (!clientId || !redirectUri) {
215
269
  console.error("Discord service configuration missing clientId or redirectUri.");
@@ -217,46 +271,132 @@ export function SignInWithDiscord({ service, onSignin, onError }) {
217
271
  return;
218
272
  }
219
273
 
274
+ if (typeof BroadcastChannel === 'undefined') {
275
+ onError?.('BroadcastChannel API is not available in this browser.');
276
+ return;
277
+ }
278
+
220
279
  try {
221
280
  const params = new URLSearchParams({
222
281
  client_id: clientId,
223
282
  redirect_uri: redirectUri,
224
283
  response_type: 'code',
225
284
  scope: scope,
285
+ state,
226
286
  });
287
+ if (prompt) params.append('prompt', prompt);
227
288
 
228
289
  const discordAuthUrl = `https://discord.com/api/oauth2/authorize?${params.toString()}`;
229
-
230
- window.open(discordAuthUrl);
290
+ window.__signinwithPendingProvider = 'discord';
291
+ window.__signinwithPendingProviderState = state;
292
+ popupRef.current = window.open(discordAuthUrl, '_blank', popupFeatures);
231
293
  } catch (error) {
232
294
  console.error("Failed to initiate Discord login:", error);
233
295
  onError?.("Failed to initiate Discord login.");
234
296
  }
235
297
  };
298
+
299
+ return (
300
+ <button className="signinwith-button signinwith-button-discord" onClick={handleDiscordLogin}>
301
+ <DiscordIcon />Continue with Discord
302
+ </button>
303
+ );
304
+ }
305
+
306
+ // Subcomponent: GitHub
307
+ export function SignInWithGithub({ service, onSignin, onError }) {
308
+ const popupRef = useRef(null);
309
+
236
310
  useEffect(() => {
237
- const handleMessage = (event) => {
238
- if (event.origin !== window.location.origin) {
239
- return; // Only accept messages from the same origin
311
+ if (typeof BroadcastChannel === 'undefined') {
312
+ console.warn('BroadcastChannel is not supported in this browser. GitHub login will not work.');
313
+ return;
314
+ }
315
+
316
+ const channel = new BroadcastChannel('signinwith-github');
317
+ const handleChannelMessage = (event) => {
318
+ const data = event.data;
319
+ if (!data || data.service !== 'github') return;
320
+
321
+ if (data.code) {
322
+ onSignin('github', { code: data.code, state: data.state });
323
+ } else if (data.error) {
324
+ onError?.(`GitHub login error: ${data.error}`);
325
+ } else {
326
+ onError?.('Unknown GitHub login error.');
240
327
  }
241
- if (event.data && event.data.type === 'discordAuth') {
242
- if (event.data.code) {
243
- onSignin('discord', { code: event.data.code });
244
- } else if (event.data.error) {
245
- onError?.(`Discord login error: ${event.data.error}`);
246
- } else {
247
- onError?.('Unknown Discord login error.');
248
- }
328
+
329
+ if (popupRef.current && !popupRef.current.closed) {
330
+ popupRef.current.close();
249
331
  }
332
+ window.__signinwithPendingProvider = null;
333
+ window.__signinwithPendingProviderState = null;
250
334
  };
251
- window.addEventListener('message', handleMessage);
335
+
336
+ channel.addEventListener('message', handleChannelMessage);
337
+
252
338
  return () => {
253
- window.removeEventListener('message', handleMessage);
339
+ channel.removeEventListener('message', handleChannelMessage);
340
+ channel.close();
254
341
  };
255
342
  }, [onSignin, onError]);
256
343
 
344
+ const handleGithubLogin = (e) => {
345
+ e.stopPropagation();
346
+ e.preventDefault();
347
+
348
+ const {
349
+ clientId,
350
+ redirectUri,
351
+ scope = 'read:user user:email',
352
+ state = 'github',
353
+ allowSignup = true,
354
+ popupFeatures = 'width=500,height=700',
355
+ codeChallenge,
356
+ codeChallengeMethod,
357
+ login,
358
+ } = service || {};
359
+
360
+ if (!clientId || !redirectUri) {
361
+ onError?.('GitHub configuration is incomplete.');
362
+ return;
363
+ }
364
+
365
+ if (typeof BroadcastChannel === 'undefined') {
366
+ onError?.('BroadcastChannel API is not available in this browser.');
367
+ return;
368
+ }
369
+
370
+ try {
371
+ const params = new URLSearchParams({
372
+ client_id: clientId,
373
+ redirect_uri: redirectUri,
374
+ response_type: 'code',
375
+ scope,
376
+ state,
377
+ allow_signup: allowSignup ? 'true' : 'false',
378
+ });
379
+ if (login) params.append('login', login);
380
+ if (codeChallenge) {
381
+ params.append('code_challenge', codeChallenge);
382
+ if (codeChallengeMethod) {
383
+ params.append('code_challenge_method', codeChallengeMethod);
384
+ }
385
+ }
386
+
387
+ const githubAuthUrl = `https://github.com/login/oauth/authorize?${params.toString()}`;
388
+ window.__signinwithPendingProvider = 'github';
389
+ window.__signinwithPendingProviderState = state;
390
+ popupRef.current = window.open(githubAuthUrl, '_blank', popupFeatures);
391
+ } catch (error) {
392
+ console.error("Failed to initiate GitHub login:", error);
393
+ onError?.("Failed to initiate GitHub login.");
394
+ }
395
+ };
396
+
257
397
  return (
258
- <button className="signinwith-button signinwith-button-discord" onClick={handleDiscordLogin}>
259
- <DiscordIcon />Continue with Discord
398
+ <button className="signinwith-button signinwith-button-github" onClick={handleGithubLogin}>
399
+ <GithubIcon />Continue with GitHub
260
400
  </button>
261
401
  );
262
402
  }
@@ -278,6 +418,8 @@ export default function SignInWith({ onSignin, onError, services, theme = 'light
278
418
  return <SignInWithApple key={key} service={config} onSignin={onSignin} onError={onError} />;
279
419
  case 'discord':
280
420
  return <SignInWithDiscord key={key} service={config} onSignin={onSignin} onError={onError} />;
421
+ case 'github':
422
+ return <SignInWithGithub key={key} service={config} onSignin={onSignin} onError={onError} />;
281
423
  default:
282
424
  console.warn(`Unsupported service key: ${key}`);
283
425
  return null;
@@ -285,4 +427,4 @@ export default function SignInWith({ onSignin, onError, services, theme = 'light
285
427
  })}
286
428
  </div>
287
429
  );
288
- }
430
+ }
package/readme.md CHANGED
@@ -4,7 +4,7 @@ A simple and straightforward library for adding "Sign in with..." buttons (Googl
4
4
 
5
5
  ## Features
6
6
 
7
- * Easy integration for Google, Facebook (Meta), Apple, and Discord sign-in.
7
+ * Easy integration for Google, Facebook (Meta), Apple, Discord, and GitHub sign-in.
8
8
  * React components for the frontend buttons.
9
9
  * Backend utility functions to verify the identity tokens/access tokens.
10
10
  * Basic customizable styling.
@@ -32,7 +32,7 @@ function App() {
32
32
  google: {
33
33
  clientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com',
34
34
  },
35
- meta: {
35
+ facebook: {
36
36
  appId: 'YOUR_FACEBOOK_APP_ID',
37
37
  },
38
38
  apple: {
@@ -44,6 +44,11 @@ function App() {
44
44
  clientId: 'YOUR_DISCORD_CLIENT_ID',
45
45
  redirectUri: '/redirect-oauth.html',
46
46
  },
47
+ github: {
48
+ clientId: 'YOUR_GITHUB_OAUTH_APP_CLIENT_ID',
49
+ redirectUri: '/redirect-oauth.html',
50
+ scope: 'read:user user:email',
51
+ },
47
52
  };
48
53
 
49
54
  // Callback function when sign-in is successful on the frontend
@@ -79,7 +84,7 @@ export default App;
79
84
 
80
85
  ### Props for `SignInWith`
81
86
 
82
- * `services` (Object, required): An object where keys are the service names (`google`, `meta`, `apple`, `discord`) and values are their respective configuration objects.
87
+ * `services` (Object, required): An object where keys are the service names (`google`, `facebook`, `apple`, `discord`, `github`) and values are their respective configuration objects.
83
88
  * `onSignin` (Function, required): A callback function that receives `(serviceName, data)` when a sign-in attempt is successful on the client-side. `data` contains the necessary information (e.g., `credential` for Google, `accessToken` for Facebook/Discord, `authorization` object for Apple) to be sent to your backend for verification.
84
89
  * `onError` (Function, optional): A callback function that receives an error string if there's an issue during the sign-in process.
85
90
 
@@ -116,6 +121,12 @@ const servicesConfig = {
116
121
  clientSecret: process.env.DISCORD_CLIENT_SECRET,
117
122
  redirectUri: process.env.DISCORD_REDIRECT_URI,
118
123
  },
124
+ github: {
125
+ clientId: process.env.GITHUB_CLIENT_ID,
126
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
127
+ redirectUri: process.env.GITHUB_REDIRECT_URI,
128
+ userAgent: 'your-app-name',
129
+ },
119
130
  };
120
131
 
121
132
  app.post('/api/auth/verify', async (req, res) => {
@@ -153,9 +164,10 @@ app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
153
164
  The main `verifySignin` function delegates to service-specific functions:
154
165
 
155
166
  * `verifyGoogleToken(servicesConfig, data)`: Verifies the Google ID token against Google's servers. It checks the token's signature, expiration, and audience (client ID).
156
- * `verifyMetaToken(servicesConfig, data)`: Verifies the Meta (Facebook) access token by calling the Facebook Graph API. It checks if the token is valid and associated with your Facebook App.
167
+ * `verifyFacebookToken(servicesConfig, data)`: Verifies the Facebook (Meta) access token by calling the Facebook Graph API. It checks if the token is valid and associated with your Facebook App.
157
168
  * `verifyAppleToken(servicesConfig, data)`: Verifies the Apple authorization code. It checks the code's validity and audience (client ID). It may require additional configuration depending on your Apple Developer setup (e.g., private key).
158
169
  * `verifyDiscordToken(servicesConfig, data)`: Exchanges the Discord authorization code for an access token, then uses the access token to fetch user information from the Discord API. It verifies that the code is valid and associated with your Discord application.
170
+ * `verifyGithubToken(servicesConfig, data)`: Exchanges the GitHub authorization code for an access token, then fetches the user's primary or any verified email via the GitHub API. Requires the OAuth app's `clientId`, `clientSecret`, and `redirectUri`.
159
171
 
160
172
  Each of these functions returns a promise that resolves to an object with either a `success: true` and the user's `email`, or `success: false` and an `error` message.
161
173
 
@@ -165,7 +177,7 @@ Basic styles are provided in `signinwith/styles.css`. You can import this file d
165
177
 
166
178
  The buttons have the base class `signinwith-button` and provider-specific classes:
167
179
  * `signinwith-button-google` (Note: Google button uses `renderButton`, styling might be limited)
168
- * `signinwith-button-meta`
180
+ * `signinwith-button-facebook`
169
181
  * `signinwith-button-apple`
170
182
  * `signinwith-button-discord`
171
183
 
@@ -194,16 +206,21 @@ You can override these styles in your own CSS. The container in the example uses
194
206
  * Set up OAuth2.
195
207
  * Add your redirect URI.
196
208
  * Get your **Client ID** and **Client Secret**. Store the client secret securely on the backend.
209
+ * **GitHub:**
210
+ * Create an OAuth App at [GitHub Developer Settings](https://github.com/settings/developers).
211
+ * Set the Authorization callback URL to your hosted `redirect-oauth.html` (or any page that forwards the OAuth code to your frontend).
212
+ * Copy the **Client ID** and **Client Secret**. Provide them to `verifySigninGithub` on the backend.
213
+ * Optional: customize scopes (`read:user user:email` by default) or `allow_signup`/`state` via the React component configuration.
197
214
 
198
215
  ## Redirect URI (Popup)
199
216
 
200
- For Discord and Apple, the `redirectUri` in the frontend configuration should point to a static HTML file (e.g., `redirect-oauth.html`) that handles the OAuth2 code. This file facilitates communication between the popup window and the main application window. Place the following content to be the page that is set as the redirectUri:
217
+ For Discord, GitHub, and Apple, the `redirectUri` in the frontend configuration should point to a static HTML file (e.g., `redirect-oauth.html`) that handles the OAuth2 code. This file facilitates communication between the popup window and the main application window. Place the following content to be the page that is set as the redirectUri:
201
218
 
202
219
  ```html
203
220
  <!DOCTYPE html>
204
221
  <html>
205
222
  <head>
206
- <title>Discord Authentication</title>
223
+ <title>OAuth Authentication</title>
207
224
  <style>
208
225
  body {
209
226
  font-family: sans-serif;
@@ -227,29 +244,57 @@ For Discord and Apple, the `redirectUri` in the frontend configuration should po
227
244
  <div class="container">
228
245
  <p>You can close this window now.</p>
229
246
  <script>
247
+ (function handleOauthResult(){
230
248
  const urlParams = new URLSearchParams(window.location.search);
231
249
  const code = urlParams.get('code');
232
250
  const error = urlParams.get('error');
233
-
234
- if (code) {
235
- window.opener.postMessage({ type: 'discordAuth', code: code }, window.location.origin);
236
- } else if(error) {
237
- window.opener.postMessage({ type: 'oauthError', error: error }, window.location.origin);
251
+ const state = urlParams.get('state');
252
+ const serviceParam = urlParams.get('service') || urlParams.get('provider');
253
+ const defaultService = window.opener && window.opener.__signinwithPendingProvider ? window.opener.__signinwithPendingProvider : null;
254
+ const fallbackState = window.opener && window.opener.__signinwithPendingProviderState ? window.opener.__signinwithPendingProviderState : null;
255
+ let service = serviceParam || defaultService || (state || '').replace(/^.*:/, '') || 'discord';
256
+ if (!service) service = 'discord';
257
+
258
+ const message = {
259
+ type: `${service}Auth`,
260
+ service,
261
+ };
262
+ if (code) message.code = code;
263
+ if (state || fallbackState) message.state = state || fallbackState;
264
+ if (error) message.error = error;
265
+
266
+ const channelName = `signinwith-${service}`;
267
+ if (typeof BroadcastChannel !== 'undefined') {
268
+ try {
269
+ const channel = new BroadcastChannel(channelName);
270
+ channel.postMessage(message);
271
+ channel.close();
272
+ } catch (err) {
273
+ console.error('Failed to broadcast OAuth result', err);
274
+ }
275
+ } else {
276
+ const target = window.opener || window.parent;
277
+ if (target && typeof target.postMessage === 'function') {
278
+ target.postMessage(message, window.location.origin);
279
+ }
238
280
  }
239
281
 
240
282
  // Attempt to close the window
241
283
  setTimeout(() => {
242
284
  window.close();
243
285
  }, 0);
286
+ })();
244
287
  </script>
245
288
  </div>
246
289
  </body>
247
290
  </html>
248
291
  ```
249
292
 
250
- This HTML file extracts the authorization code (or error) from the URL hash and sends it back to the main window using `postMessage`. The main application needs to listen for this message and then proceed with the backend verification. This approach is necessary because Discord's OAuth flow, when initiated in a popup, requires a way to pass the authorization code back to the originating window.
293
+ This HTML file extracts the authorization code (or error) from the URL hash and sends it back to the main window using `BroadcastChannel` (falling back to `postMessage` when unavailable). The main application needs to listen for this message and then proceed with the backend verification. This approach is necessary because Discord's OAuth flow, when initiated in a popup, requires a way to pass the authorization code back to the originating window.
294
+
295
+ The helper page uses the `BroadcastChannel` API to publish the OAuth result back to the main window and automatically infers the provider name (Discord, GitHub, etc.) based on the pending popup initiation, so you can re-use the same redirect page for every provider that relies on a popup + code flow.
251
296
 
252
297
  Note: you can import this page as a string (to then return it as a response from your server) via:
253
298
  ```javascript
254
299
  import htmlContent from 'signinwith/redirect-oauth.html?raw';
255
- ```
300
+ ```
@@ -1,7 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title>Discord Authentication</title>
4
+ <title>OAuth Authentication</title>
5
5
  <style>
6
6
  body {
7
7
  font-family: sans-serif;
@@ -24,22 +24,48 @@
24
24
  <body>
25
25
  <div class="container">
26
26
  <p>You can close this window now.</p>
27
- <script>
28
- const urlParams = new URLSearchParams(window.location.search);
29
- const code = urlParams.get('code');
30
- const error = urlParams.get('error');
27
+ <script>
28
+ (function handleOauthResult() {
29
+ const urlParams = new URLSearchParams(window.location.search);
30
+ const code = urlParams.get('code');
31
+ const error = urlParams.get('error');
32
+ const state = urlParams.get('state');
33
+ const serviceParam = urlParams.get('service') || urlParams.get('provider');
34
+ const defaultService = window.opener && window.opener.__signinwithPendingProvider ? window.opener.__signinwithPendingProvider : null;
35
+ const fallbackState = window.opener && window.opener.__signinwithPendingProviderState ? window.opener.__signinwithPendingProviderState : null;
36
+ let service = serviceParam || defaultService || (state || '').replace(/^.*:/, '') || 'discord';
37
+ if (!service) service = 'discord';
31
38
 
32
- if (code) {
33
- window.opener.postMessage({ type: 'discordAuth', code: code }, window.location.origin);
34
- } else if(error) {
35
- window.opener.postMessage({ type: 'oauthError', error: error }, window.location.origin);
36
- }
39
+ const message = {
40
+ type: `${service}Auth`,
41
+ service,
42
+ };
43
+ if (code) message.code = code;
44
+ if (state || fallbackState) message.state = state || fallbackState;
45
+ if (error) message.error = error;
37
46
 
38
- // Attempt to close the window
39
- setTimeout(() => {
40
- window.close();
41
- }, 0);
42
- </script>
47
+ const channelName = `signinwith-${service}`;
48
+ if (typeof BroadcastChannel !== 'undefined') {
49
+ try {
50
+ const channel = new BroadcastChannel(channelName);
51
+ channel.postMessage(message);
52
+ channel.close();
53
+ } catch (err) {
54
+ console.error('Failed to broadcast OAuth result', err);
55
+ }
56
+ } else {
57
+ const target = window.opener || window.parent;
58
+ if (target && typeof target.postMessage === 'function') {
59
+ target.postMessage(message, window.location.origin);
60
+ }
61
+ }
62
+
63
+ // Attempt to close the window
64
+ setTimeout(() => {
65
+ window.close();
66
+ }, 0);
67
+ })();
68
+ </script>
43
69
  </div>
44
70
  </body>
45
- </html>
71
+ </html>
package/styles.css CHANGED
@@ -57,10 +57,20 @@
57
57
  background-color: #000000;
58
58
  color: white;
59
59
  border-color: #000000;
60
-
60
+
61
61
  &:hover {
62
62
  background-color: #333333;
63
63
  }
64
64
  }
65
+
66
+ &.signinwith-button-github {
67
+ background-color: #24292f;
68
+ color: white;
69
+ border-color: #24292f;
70
+
71
+ &:hover {
72
+ background-color: #1b1f23;
73
+ }
74
+ }
65
75
  }
66
- }
76
+ }