signinwith 1.0.1 → 1.0.3

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.
Files changed (5) hide show
  1. package/index.js +24 -15
  2. package/package.json +4 -5
  3. package/react.jsx +217 -68
  4. package/readme.md +170 -107
  5. package/styles.css +50 -51
package/index.js CHANGED
@@ -1,13 +1,7 @@
1
- import { OAuth2Client } from 'google-auth-library';
2
- import appleSigninAuth from 'apple-signin-auth';
3
-
4
1
  export const verifySigninGoogle = async (config, verificationData) => {
5
- const client = new OAuth2Client(config.clientId);
6
- const ticket = await client.verifyIdToken({
7
- idToken: verificationData.credential,
8
- audience: config.clientId,
9
- });
10
- const payload = ticket.getPayload();
2
+ const res = await fetch(`https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${verificationData.credential}`);
3
+ const payload = await res.json();
4
+ if (payload.aud !== config.clientId) return { success: false, error: 'Invalid aud' };
11
5
  return payload.email ? { success: true, email: payload.email } : { success: false, error: 'Email not found' };
12
6
  };
13
7
 
@@ -19,18 +13,33 @@ export const verifySigninMeta = async (config, verificationData) => {
19
13
 
20
14
  export const verifySigninApple = async (config, verificationData) => {
21
15
  const { id_token } = verificationData;
22
- const result = await appleSigninAuth.verifyIdToken(id_token, {
23
- audience: config.clientId,
24
- ignoreExpiration: true,
16
+ const form = new FormData();
17
+ form.append('id_token', id_token);
18
+ form.append('client_id', config.clientId);
19
+ const res = await fetch('https://appleid.apple.com/auth/verify', {
20
+ method: 'POST',
21
+ body: form,
25
22
  });
23
+ const result = await res.json();
24
+ if (!result.success) return { success: false, error: result.error || 'Invalid Apple signin' };
26
25
  return result.email ? { success: true, email: result.email } : { success: false, error: 'Email not available from Apple' };
27
26
  };
27
+ export const verifySigninDiscord = async (config, verificationData) => {
28
+ const res = await fetch('https://discord.com/api/v10/users/@me', {
29
+ headers: {
30
+ authorization: `Bearer ${verificationData.accessToken}`,
31
+ },
32
+ });
33
+ const profile = await res.json();
34
+ return profile.email ? { success: true, email: profile.email } : { success: false, error: 'Email not available from Discord' };
35
+ };
28
36
 
29
37
  export default verifySignin = async (services, service, verificationData) => {
30
38
  try {
31
- if (service === 'google') return await verifySigninGoogle(services.google, verificationData);
32
- if (service === 'meta') return await verifySigninMeta(services.meta, verificationData);
33
- if (service === 'apple') return await verifySigninApple(services.apple, verificationData);
39
+ if (services.google && service === 'google') return await verifySigninGoogle(services.google, verificationData);
40
+ if (services.meta && service === 'meta') return await verifySigninMeta(services.meta, verificationData);
41
+ if (services.apple && service === 'apple') return await verifySigninApple(services.apple, verificationData);
42
+ if (services.discord && service === 'discord') return await verifySigninDiscord(services.discord, verificationData);
34
43
  return { success: false, error: 'Unsupported service' };
35
44
  } catch (err) {
36
45
  return { success: false, error: err.message || 'Unknown error' };
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "signinwith",
3
- "version": "1.0.1",
4
- "description": "Simple and straightforward library for sign in / sign up with thirdparty oAuth services like Google, Meta, Apple...",
3
+ "version": "1.0.3",
4
+ "description": "Simple and straightforward library for sign in / sign up with thirdparty oAuth services like Google, Meta, Apple, Discord...",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  },
8
8
  "type": "module",
9
9
  "exports": {
10
10
  ".": "./index.js",
11
- "./react": "./react.jsx"
11
+ "./react": "./react.jsx",
12
+ "./styles.css": "./styles.css"
12
13
  },
13
14
  "keywords": [
14
15
  "signinwith",
@@ -27,8 +28,6 @@
27
28
  "author": "ybouane",
28
29
  "license": "ISC",
29
30
  "dependencies": {
30
- "apple-signin-auth": "^2.0.0",
31
- "google-auth-library": "^9.15.1",
32
31
  "react": "^19.1.0"
33
32
  }
34
33
  }
package/react.jsx CHANGED
@@ -1,34 +1,24 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
2
 
3
- // Subcomponent: Meta (Facebook)
4
3
  const FacebookIcon = () => (
5
- <svg width="20" height="20" viewBox="0 0 32 32" fill="none">
6
- <rect width="32" height="32" rx="16" fill="#1877F2"/>
7
- <path d="M22 16.001h-3v8h-4v-8h-2v-3h2v-2c0-2.206 1.794-4 4-4h3v3h-3c-.553 0-1 .447-1 1v2h4l-1 3z" fill="#fff"/>
8
- </svg>
4
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 666.667 666.667"><defs><clipPath clipPathUnits="userSpaceOnUse" id="a"><path d="M0 700h700V0H0Z"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(1.33333 0 0 -1.33333 -133.333 800)"><path d="M600 350c0 138.071-111.929 250-250 250S100 488.071 100 350c0-117.245 80.715-215.622 189.606-242.638v166.242h-51.552V350h51.552v32.919c0 85.092 38.508 124.532 122.048 124.532 15.838 0 43.167-3.105 54.347-6.211v-69.254c-5.901.621-16.149.932-28.882.932-40.993 0-56.832-15.528-56.832-55.9V350h81.659l-14.028-76.396h-67.631V101.831C504.073 116.782 600 222.182 600 350" fill="#0866ff"/><path d="M447.918 273.604 461.947 350h-81.66v27.019c0 40.372 15.839 55.899 56.832 55.899 12.733 0 22.98-.31 28.882-.931v69.253c-11.18 3.106-38.51 6.212-54.347 6.212-83.54 0-122.048-39.441-122.048-124.533v-32.92h-51.552v-76.395h51.552V107.362A250.559 250.559 0 0 1 350 100c10.254 0 20.358.632 30.288 1.83v171.774Z" fill="#fff"/></g></svg>
9
5
  );
10
6
 
11
7
  const GoogleIcon = () => (
12
- <svg width="20" height="20" viewBox="0 0 32 32" fill="none">
13
- <g>
14
- <circle cx="16" cy="16" r="16" fill="#fff"/>
15
- <path d="M27 16.082c0-.638-.057-1.252-.163-1.837H16v3.478h6.18a5.29 5.29 0 0 1-2.293 3.47v2.885h3.7C25.98 22.13 27 19.345 27 16.082z" fill="#4285F4"/>
16
- <path d="M16 27c2.43 0 4.47-.805 5.96-2.19l-3.7-2.885c-1.03.69-2.35 1.1-3.77 1.1-2.9 0-5.36-1.96-6.24-4.6h-3.8v2.89A10.997 10.997 0 0 0 16 27z" fill="#34A853"/>
17
- <path d="M9.76 18.425A6.594 6.594 0 0 1 9.2 16c0-.84.15-1.655.41-2.425v-2.89h-3.8A10.997 10.997 0 0 0 5 16c0 1.73.41 3.37 1.17 4.825l3.59-2.4z" fill="#FBBC05"/>
18
- <path d="M16 9.6c1.32 0 2.5.455 3.43 1.35l2.57-2.57C20.47 6.81 18.43 6 16 6A10.997 10.997 0 0 0 5.81 10.685l3.8 2.89C10.64 11.56 13.1 9.6 16 9.6z" fill="#EA4335"/>
19
- </g>
20
- </svg>
8
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/><path d="M1 1h22v22H1z" fill="none"/></svg>
21
9
  );
22
10
 
23
11
  const AppleIcon = () => (
24
- <svg width="20" height="20" viewBox="0 0 32 32" fill="none">
25
- <g>
26
- <circle cx="16" cy="16" r="16" fill="#000"/>
27
- <path d="M22.67 23.13c-.53 1.13-1.16 2.25-2.08 2.27-.89.02-1.18-.72-2.45-.72-1.27 0-1.6.7-2.44.74-.98.04-1.73-1.22-2.27-2.34-1.24-2.54-2.18-7.19.09-8.97.86-.67 2.09-.47 3.36-.47 1.26 0 2.46-.21 3.36.47 1.03.8 1.45 2.22 1.2 3.44-.25 1.22-1.01 1.82-1.01 1.82s1.13.18 1.99 1.13c.86.95.7 2.36.18 3.13zM18.6 8.8c.59-.7 1.01-1.68.89-2.67-.86.04-1.89.57-2.5 1.27-.55.62-1.04 1.62-.86 2.58.97.08 1.88-.49 2.47-1.18z" fill="#fff"/>
28
- </g>
29
- </svg>
12
+ <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="814" height="1000"><path fill="#FFFFFF" d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/></svg>
30
13
  );
31
- export function SignInWithMeta({ service, onSignin }) {
14
+
15
+ // Subcomponent: Discord Icon
16
+ const DiscordIcon = () => (
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
+ );
19
+
20
+ // Subcomponent: Facebook
21
+ export function SignInWithFacebook({ service, onSignin, onError }) {
32
22
  useEffect(() => {
33
23
  if (!window.FB) {
34
24
  window.fbAsyncInit = function () {
@@ -50,49 +40,86 @@ export function SignInWithMeta({ service, onSignin }) {
50
40
  }
51
41
  }, [service.appId]);
52
42
 
53
- const handleLogin = () => {
43
+ const handleLogin = (e) => {
44
+ e.stopPropagation();
45
+ e.preventDefault();
54
46
  window.FB.login(function (response) {
55
47
  if (response.authResponse) {
56
- onSignin('meta', { accessToken: response.authResponse.accessToken });
48
+ onSignin('facebook', { accessToken: response.authResponse.accessToken });
49
+ } else {
50
+ onError?.('Failed to log in with Facebook.');
57
51
  }
58
52
  }, { scope: 'email,public_profile' });
59
53
  };
60
54
 
61
- return <button className="signinwith-button signinwith-button-meta" onClick={handleLogin}><FacebookIcon />Continue with Facebook</button>;
55
+ return <button className="signinwith-button signinwith-button-facebook" onClick={handleLogin}><FacebookIcon />Continue with Facebook</button>;
62
56
  }
57
+
63
58
  // Subcomponent: Google
64
- export function SignInWithGoogle({ service, onSignin }) {
59
+ export function SignInWithGoogle({ service, onSignin, onError }) {
65
60
  useEffect(() => {
66
- const script = document.createElement('script');
67
- script.src = 'https://accounts.google.com/gsi/client';
68
- script.async = true;
69
- script.defer = true;
70
- script.onload = () => {
61
+ const scriptId = 'google-gsi-script';
62
+ let script = document.getElementById(scriptId);
63
+
64
+ if (!script) {
65
+ script = document.createElement('script');
66
+ script.id = scriptId;
67
+ script.src = 'https://accounts.google.com/gsi/client';
68
+ script.async = true;
69
+ script.defer = true;
70
+ script.onload = () => {
71
+ if (window.google?.accounts?.id) {
72
+ window.google.accounts.id.initialize({
73
+ client_id: service.clientId,
74
+ use_fedcm_for_prompt: true,
75
+ callback: (res) => {
76
+ onSignin('google', { credential: res.credential });
77
+ }
78
+ });
79
+ } else {
80
+ onError?.('Google Sign-In script loaded but initialization failed.');
81
+ }
82
+ };
83
+ script.onerror = () => {
84
+ onError?.('Failed to load Google Sign-In script.');
85
+ };
86
+ document.body.appendChild(script);
87
+ } else if (window.google?.accounts?.id) {
88
+ // If script already exists, ensure it's initialized (might be needed if component remounts)
71
89
  window.google.accounts.id.initialize({
72
90
  client_id: service.clientId,
91
+ use_fedcm_for_prompt: true,
73
92
  callback: (res) => {
74
93
  onSignin('google', { credential: res.credential });
75
94
  }
76
95
  });
77
- };
78
- document.body.appendChild(script);
96
+ }
97
+
79
98
 
99
+ // Cleanup function does not remove the script to allow multiple instances
100
+ // or remounts without reloading. Google's script handles this internally.
80
101
  return () => {
81
- const scriptTag = document.querySelector('script[src="https://accounts.google.com/gsi/client"]');
82
- if (scriptTag) {
83
- document.body.removeChild(scriptTag);
84
- }
102
+ // Optional: You might want to hide the prompt if the component unmounts
103
+ // window.google?.accounts?.id?.cancel();
85
104
  };
86
- }, [service.clientId]);
87
-
88
- const handleGoogleLogin = () => {
89
- window.google.accounts.id.requestCredential({
90
- clientId: service.clientId,
91
- callback: (res) => {
92
- onSignin('google', { credential: res.credential });
93
- },
94
- scopes: ['email', 'profile']
95
- });
105
+ }, [service.clientId, onSignin, onError]);
106
+
107
+ const handleGoogleLogin = (e) => {
108
+ e.stopPropagation();
109
+ e.preventDefault();
110
+ if (window.google?.accounts?.id) {
111
+ window.google.accounts.id.prompt((notification) => {
112
+ // Handle prompt UI notifications (e.g., closed, error)
113
+ if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
114
+ // Potentially trigger a backup UX or just log
115
+ onError?.(notification.getNotDisplayedReason() || notification.getSkippedReason() || 'Google Sign-In prompt was not displayed or was skipped.');
116
+ } else if (notification.isDismissedMoment()) {
117
+ onError?.(notification.getDismissedReason() || 'Google Sign-In prompt was dismissed.');
118
+ }
119
+ });
120
+ } else {
121
+ onError?.('Google Sign-In is not initialized.');
122
+ }
96
123
  };
97
124
 
98
125
  return (
@@ -103,36 +130,158 @@ export function SignInWithGoogle({ service, onSignin }) {
103
130
  }
104
131
 
105
132
  // Subcomponent: Apple
106
- export function SignInWithApple({ service, onSignin }) {
133
+ export function SignInWithApple({ service, onSignin, onError }) {
107
134
  useEffect(() => {
108
- const script = document.createElement('script');
109
- script.src = 'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js';
110
- script.onload = () => {
111
- window.AppleID.auth.init({
112
- clientId: service.clientId,
113
- scope: 'email name',
114
- redirectURI: window.location.origin + '/auth/apple/callback',
115
- usePopup: true,
116
- });
117
- window.addEventListener('AppleIDSignInOnSuccess', (e) => {
118
- onSignin('apple', e.detail.authorization);
135
+ const scriptId = 'apple-auth-script';
136
+ let script = document.getElementById(scriptId);
137
+
138
+ const handleAppleSignInSuccess = (event) => {
139
+ onSignin('apple', event.detail.authorization);
140
+ };
141
+ const handleAppleSignInFailure = (event) => {
142
+ onError?.(event.detail.error || 'Sign in with Apple failed.');
143
+ };
144
+
145
+ if (!script) {
146
+ script = document.createElement('script');
147
+ script.id = scriptId;
148
+ script.src = 'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js';
149
+ script.async = true;
150
+ script.defer = true;
151
+ script.onload = () => {
152
+ try {
153
+ window.AppleID.auth.init({
154
+ clientId: service.clientId,
155
+ scope: service.scope || 'email name', // Default scope
156
+ redirectURI: service.redirectUri, // Default redirect URI
157
+ usePopup: true,
158
+ });
159
+ } catch (error) {
160
+ console.error("Apple Sign In initialization failed:", error);
161
+ onError?.('Failed to initialize Apple Sign In.');
162
+ return; // Stop if init fails
163
+ }
164
+ // Add event listeners after successful initialization
165
+ document.addEventListener('AppleIDSignInOnSuccess', handleAppleSignInSuccess);
166
+ document.addEventListener('AppleIDSignInOnFailure', handleAppleSignInFailure);
167
+ };
168
+ script.onerror = () => {
169
+ onError?.('Failed to load Apple Sign In script.');
170
+ };
171
+ document.body.appendChild(script);
172
+ } else {
173
+ // If script exists, re-add listeners in case component remounted
174
+ document.removeEventListener('AppleIDSignInOnSuccess', handleAppleSignInSuccess);
175
+ document.removeEventListener('AppleIDSignInOnFailure', handleAppleSignInFailure);
176
+ document.addEventListener('AppleIDSignInOnSuccess', handleAppleSignInSuccess);
177
+ document.addEventListener('AppleIDSignInOnFailure', handleAppleSignInFailure);
178
+ }
179
+
180
+ return () => {
181
+ // Cleanup listeners when component unmounts
182
+ document.removeEventListener('AppleIDSignInOnSuccess', handleAppleSignInSuccess);
183
+ document.removeEventListener('AppleIDSignInOnFailure', handleAppleSignInFailure);
184
+ };
185
+ }, [service.clientId, service.scope, service.redirectUri, service.usePopup, onSignin, onError]);
186
+
187
+ const handleAppleLogin = async (e) => {
188
+ e.stopPropagation();
189
+ e.preventDefault();
190
+ try {
191
+ if (window.AppleID?.auth) {
192
+ await window.AppleID.auth.signIn();
193
+ } else {
194
+ onError?.('Apple Sign In is not initialized.');
195
+ }
196
+ } catch (error) {
197
+ // This catch might handle errors from the signIn() call itself,
198
+ // though the event listener is the primary mechanism.
199
+ console.error("Apple Sign In error:", error);
200
+ onError?.('An error occurred during Apple Sign In.');
201
+ }
202
+ };
203
+ return <button className="signinwith-button signinwith-button-apple" onClick={handleAppleLogin}><AppleIcon />Continue with Apple</button>;
204
+ }
205
+
206
+ // Subcomponent: Discord
207
+ export function SignInWithDiscord({ service, onSignin, onError }) {
208
+ const handleDiscordLogin = (e) => {
209
+ e.stopPropagation();
210
+ e.preventDefault();
211
+
212
+ const { clientId, redirectUri, scope = 'identify email' } = service; // Default scopes
213
+
214
+ if (!clientId || !redirectUri) {
215
+ console.error("Discord service configuration missing clientId or redirectUri.");
216
+ onError?.("Discord configuration is incomplete.");
217
+ return;
218
+ }
219
+
220
+ try {
221
+ const params = new URLSearchParams({
222
+ client_id: clientId,
223
+ redirect_uri: redirectUri,
224
+ response_type: 'code',
225
+ scope: scope,
119
226
  });
227
+
228
+ const discordAuthUrl = `https://discord.com/api/oauth2/authorize?${params.toString()}`;
229
+
230
+ window.open(discordAuthUrl);
231
+ } catch (error) {
232
+ console.error("Failed to initiate Discord login:", error);
233
+ onError?.("Failed to initiate Discord login.");
234
+ }
235
+ };
236
+ useEffect(() => {
237
+ const handleMessage = (event) => {
238
+ if (event.origin !== window.location.origin) {
239
+ return; // Only accept messages from the same origin
240
+ }
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
+ }
249
+ }
250
+ };
251
+ window.addEventListener('message', handleMessage);
252
+ return () => {
253
+ window.removeEventListener('message', handleMessage);
120
254
  };
121
- document.body.appendChild(script);
122
- }, [service.clientId]);
255
+ }, [onSignin, onError]);
123
256
 
124
- return <button className="signinwith-button signinwith-button-apple" onClick={() => window.AppleID.auth.signIn()}><AppleIcon />Continue with Apple</button>;
257
+ return (
258
+ <button className="signinwith-button signinwith-button-discord" onClick={handleDiscordLogin}>
259
+ <DiscordIcon />Continue with Discord
260
+ </button>
261
+ );
125
262
  }
126
263
 
264
+
127
265
  // Main SignInWith Component
128
- export default function SignInWith({ onSignin, services, theme = 'light' }) {
266
+ export default function SignInWith({ onSignin, onError, services, theme = 'light' }) {
129
267
  return (
130
- <div style={{ display: 'flex', gap: '1rem', flexDirection: 'column' }}>
131
- {Object.entries(services).map(([key, config]) => {
132
- if (key === 'google') return <SignInWithGoogle key={key} service={config} onSignin={onSignin} />;
133
- if (key === 'meta') return <SignInWithMeta key={key} service={config} onSignin={onSignin} />;
134
- if (key === 'apple') return <SignInWithApple key={key} service={config} onSignin={onSignin} />;
135
- return null;
268
+ <div className={`signinwith-container signinwith-theme-${theme}`}>
269
+ {Object.entries(services || {}).map(([key, config]) => { // Added default {} for services
270
+ if (!config) return null; // Skip if config is null/undefined
271
+
272
+ switch (key.toLowerCase()) { // Use lowercase key for case-insensitivity
273
+ case 'google':
274
+ return <SignInWithGoogle key={key} service={config} onSignin={onSignin} onError={onError} />;
275
+ case 'facebook':
276
+ return <SignInWithFacebook key={key} service={config} onSignin={onSignin} onError={onError} />;
277
+ case 'apple':
278
+ return <SignInWithApple key={key} service={config} onSignin={onSignin} onError={onError} />;
279
+ case 'discord':
280
+ return <SignInWithDiscord key={key} service={config} onSignin={onSignin} onError={onError} />;
281
+ default:
282
+ console.warn(`Unsupported service key: ${key}`);
283
+ return null;
284
+ }
136
285
  })}
137
286
  </div>
138
287
  );
package/readme.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Sign In With
2
2
 
3
- A simple and straightforward library for adding "Sign in with..." buttons (Google, Meta/Facebook, Apple) to your web application, handling both the frontend UI (React) and backend verification.
3
+ A simple and straightforward library for adding "Sign in with..." buttons (Google, Facebook/Meta, Apple, Discord) to your web application, handling both the frontend UI (React) and backend verification.
4
4
 
5
5
  ## Features
6
6
 
7
- * Easy integration for Google, Meta (Facebook), and Apple sign-in.
7
+ * Easy integration for Google, Facebook (Meta), Apple, and Discord 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.
@@ -27,47 +27,51 @@ import SignInWith from 'signinwith/react';
27
27
  import 'signinwith/styles.css'; // Import the styles
28
28
 
29
29
  function App() {
30
- // Configuration for the services you want to enable
31
- const services = {
32
- google: {
33
- clientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com',
34
- },
35
- meta: {
36
- appId: 'YOUR_FACEBOOK_APP_ID',
37
- },
38
- apple: {
39
- clientId: 'YOUR_APPLE_SERVICE_ID', // e.g., com.mywebsite.signin
40
- // redirectURI is automatically set to window.location.origin + '/auth/apple/callback'
41
- // Ensure this callback route exists or handle the popup flow appropriately.
42
- },
43
- };
44
-
45
- // Callback function when sign-in is successful on the frontend
46
- const handleSignin = (service, data) => {
47
- console.log(`Signed in with ${service}:`, data);
48
- // Send 'service' and 'data' to your backend for verification
49
- fetch('/api/auth/verify', {
50
- method: 'POST',
51
- headers: { 'Content-Type': 'application/json' },
52
- body: JSON.stringify({ service, data }),
53
- })
54
- .then(res => res.json())
55
- .then(result => {
56
- if (result.success) {
57
- console.log('Backend verification successful:', result.email);
58
- // Proceed with user session, redirect, etc.
59
- } else {
60
- console.error('Backend verification failed:', result.error);
61
- }
62
- });
63
- };
64
-
65
- return (
66
- <div className="signinwith-container">
67
- <h2>Sign In</h2>
68
- <SignInWith services={services} onSignin={handleSignin} />
69
- </div>
70
- );
30
+ // Configuration for the services you want to enable
31
+ const services = {
32
+ google: {
33
+ clientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com',
34
+ },
35
+ meta: {
36
+ appId: 'YOUR_FACEBOOK_APP_ID',
37
+ },
38
+ apple: {
39
+ clientId: 'YOUR_APPLE_SERVICE_ID', // e.g., com.mywebsite.signin
40
+ redirectUri: '/redirect-oauth.html'
41
+ // Ensure this callback route exists or handle the popup flow appropriately.
42
+ },
43
+ discord: {
44
+ clientId: 'YOUR_DISCORD_CLIENT_ID',
45
+ redirectUri: '/redirect-oauth.html',
46
+ },
47
+ };
48
+
49
+ // Callback function when sign-in is successful on the frontend
50
+ const handleSignin = (service, data) => {
51
+ console.log(`Signed in with ${service}:`, data);
52
+ // Send 'service' and 'data' to your backend for verification
53
+ fetch('/api/auth/verify', {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify({ service, data }),
57
+ })
58
+ .then(res => res.json())
59
+ .then(result => {
60
+ if (result.success) {
61
+ console.log('Backend verification successful:', result.email);
62
+ // Proceed with user session, redirect, etc.
63
+ } else {
64
+ console.error('Backend verification failed:', result.error);
65
+ }
66
+ });
67
+ };
68
+
69
+ return (
70
+ <div className="signinwith-container">
71
+ <h2>Sign In</h2>
72
+ <SignInWith services={services} onSignin={handleSignin} onError={error=>console.error(error)} />
73
+ </div>
74
+ );
71
75
  }
72
76
 
73
77
  export default App;
@@ -75,8 +79,9 @@ export default App;
75
79
 
76
80
  ### Props for `SignInWith`
77
81
 
78
- * `services` (Object, required): An object where keys are the service names (`google`, `meta`, `apple`) and values are their respective configuration objects.
79
- * `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 Meta, `authorization` object for Apple) to be sent to your backend for verification.
82
+ * `services` (Object, required): An object where keys are the service names (`google`, `meta`, `apple`, `discord`) and values are their respective configuration objects.
83
+ * `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
+ * `onError` (Function, optional): A callback function that receives an error string if there's an issue during the sign-in process.
80
85
 
81
86
  ## Backend Verification
82
87
 
@@ -93,44 +98,49 @@ app.use(express.json());
93
98
  // Your service configurations (should match frontend, plus any secrets)
94
99
  // Store these securely, e.g., in environment variables
95
100
  const servicesConfig = {
96
- google: {
97
- clientId: process.env.GOOGLE_CLIENT_ID,
98
- },
99
- meta: {
100
- // Meta verification only needs the access token from the frontend
101
- // No specific backend config needed here for the library function itself
102
- // but you might need App ID/Secret for other Graph API calls.
103
- },
104
- apple: {
105
- clientId: process.env.APPLE_CLIENT_ID, // Your Service ID (e.g., com.mywebsite.signin)
106
- // The library uses apple-signin-auth which might require more config
107
- // depending on how you generated your keys. Refer to its documentation.
108
- },
101
+ google: {
102
+ clientId: process.env.GOOGLE_CLIENT_ID,
103
+ },
104
+ facebook: {
105
+ // Facebook verification only needs the access token from the frontend
106
+ // No specific backend config needed here for the library function itself
107
+ // but you might need App ID/Secret for other Graph API calls.
108
+ },
109
+ apple: {
110
+ clientId: process.env.APPLE_CLIENT_ID, // Your Service ID (e.g., com.mywebsite.signin)
111
+ // The library uses apple-signin-auth which might require more config
112
+ // depending on how you generated your keys. Refer to its documentation.
113
+ },
114
+ discord: {
115
+ clientId: process.env.DISCORD_CLIENT_ID,
116
+ clientSecret: process.env.DISCORD_CLIENT_SECRET,
117
+ redirectUri: process.env.DISCORD_REDIRECT_URI,
118
+ },
109
119
  };
110
120
 
111
121
  app.post('/api/auth/verify', async (req, res) => {
112
- const { service, data } = req.body;
113
-
114
- if (!service || !data) {
115
- return res.status(400).json({ success: false, error: 'Missing service or data' });
116
- }
117
-
118
- try {
119
- const result = await verifySignin(servicesConfig, service, data);
120
- if (result.success) {
121
- // Verification successful!
122
- // Find or create user with result.email
123
- console.log(`Verified ${service} sign in for: ${result.email}`);
124
- res.json({ success: true, email: result.email });
125
- } else {
126
- // Verification failed
127
- console.error(`Failed to verify ${service}: ${result.error}`);
128
- res.status(401).json({ success: false, error: result.error || 'Verification failed' });
129
- }
130
- } catch (error) {
131
- console.error(`Error during ${service} verification:`, error);
132
- res.status(500).json({ success: false, error: 'Internal server error' });
133
- }
122
+ const { service, data } = req.body;
123
+
124
+ if (!service || !data) {
125
+ return res.status(400).json({ success: false, error: 'Missing service or data' });
126
+ }
127
+
128
+ try {
129
+ const result = await verifySignin(servicesConfig, service, data);
130
+ if (result.success) {
131
+ // Verification successful!
132
+ // Find or create user with result.email
133
+ console.log(`Verified ${service} sign in for: ${result.email}`);
134
+ res.json({ success: true, email: result.email });
135
+ } else {
136
+ // Verification failed
137
+ console.error(`Failed to verify ${service}: ${result.error}`);
138
+ res.status(401).json({ success: false, error: result.error || 'Verification failed' });
139
+ }
140
+ } catch (error) {
141
+ console.error(`Error during ${service} verification:`, error);
142
+ res.status(500).json({ success: false, error: 'Internal server error' });
143
+ }
134
144
  });
135
145
 
136
146
  const PORT = process.env.PORT || 3001;
@@ -138,19 +148,16 @@ app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
138
148
 
139
149
  ```
140
150
 
141
- ### Verification Functions
151
+ ### Verification
142
152
 
143
153
  The main `verifySignin` function delegates to service-specific functions:
144
154
 
145
- * `verifySigninGoogle(config, verificationData)`: Verifies Google ID token.
146
- * `config`: Needs `{ clientId }`.
147
- * `verificationData`: Needs `{ credential }`.
148
- * `verifySigninMeta(config, verificationData)`: Verifies Meta access token by fetching user email.
149
- * `config`: Not directly used by the function, but you need your App ID configured on the frontend.
150
- * `verificationData`: Needs `{ accessToken }`.
151
- * `verifySigninApple(config, verificationData)`: Verifies Apple ID token.
152
- * `config`: Needs `{ clientId }` (Your Apple Service ID).
153
- * `verificationData`: Needs `{ id_token }` (from the `authorization` object).
155
+ * `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.
157
+ * `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
+ * `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.
159
+
160
+ 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.
154
161
 
155
162
  ## Styling
156
163
 
@@ -160,28 +167,84 @@ The buttons have the base class `signinwith-button` and provider-specific classe
160
167
  * `signinwith-button-google` (Note: Google button uses `renderButton`, styling might be limited)
161
168
  * `signinwith-button-meta`
162
169
  * `signinwith-button-apple`
170
+ * `signinwith-button-discord`
163
171
 
164
172
  You can override these styles in your own CSS. The container in the example uses `signinwith-container` for layout.
165
173
 
166
174
  ## Configuration Details
167
175
 
168
176
  * **Google:**
169
- * Create a project in [Google Cloud Console](https://console.cloud.google.com/).
170
- * Set up OAuth 2.0 Credentials (Web application type).
171
- * Add your domain(s) to "Authorized JavaScript origins".
172
- * Add your backend callback URL (if applicable) to "Authorized redirect URIs".
173
- * Get your **Client ID**.
174
- * **Meta (Facebook):**
175
- * Create an App at [Meta for Developers](https://developers.facebook.com/).
176
- * Set up "Facebook Login for Business".
177
- * Add your domain(s) to the App Domains and Site URL in the app settings.
178
- * Get your **App ID**.
177
+ * Create a project in [Google Cloud Console](https://console.cloud.google.com/).
178
+ * Set up OAuth 2.0 Credentials (Web application type).
179
+ * Add your domain(s) to "Authorized JavaScript origins".
180
+ * Add your backend callback URL (if applicable) to "Authorized redirect URIs".
181
+ * Get your **Client ID**.
182
+ * **Facebook (Meta):**
183
+ * Create an App at [Facebook for Developers](https://developers.facebook.com/).
184
+ * Set up "Facebook Login for Business".
185
+ * Add your domain(s) to the App Domains and Site URL in the app settings.
186
+ * Get your **App ID**.
179
187
  * **Apple:**
180
- * Register an App ID and a Service ID in your [Apple Developer Account](https://developer.apple.com/).
181
- * Configure "Sign in with Apple" for your App ID.
182
- * Associate your domain(s) with the Service ID and verify them.
183
- * Get your **Service ID** (used as `clientId`). You might also need a private key for certain backend operations, but `apple-signin-auth` handles basic token verification with just the `clientId` (audience check). Ensure the frontend `redirectURI` is correctly handled (either via a backend route or popup flow).
184
-
185
- ## License
188
+ * Register an App ID and a Service ID in your [Apple Developer Account](https://developer.apple.com/).
189
+ * Configure "Sign in with Apple" for your App ID.
190
+ * Associate your domain(s) with the Service ID and verify them.
191
+ * Get your **Service ID** (used as `clientId`). You might also need a private key for certain backend operations, but `apple-signin-auth` handles basic token verification with just the `clientId` (audience check). Ensure the frontend `redirectUri` is correctly handled (either via a backend route or popup flow).
192
+ * **Discord:**
193
+ * Create an App at [Discord Developer Portal](https://discord.com/developers/applications).
194
+ * Set up OAuth2.
195
+ * Add your redirect URI.
196
+ * Get your **Client ID** and **Client Secret**. Store the client secret securely on the backend.
197
+
198
+ ## Redirect URI (Popup)
199
+
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:
201
+
202
+ ```html
203
+ <!DOCTYPE html>
204
+ <html>
205
+ <head>
206
+ <title>Discord Authentication</title>
207
+ <style>
208
+ body {
209
+ font-family: sans-serif;
210
+ display: flex;
211
+ justify-content: center;
212
+ align-items: center;
213
+ height: 100vh;
214
+ margin: 0;
215
+ background-color: #f0f0f0;
216
+ }
217
+ .container {
218
+ text-align: center;
219
+ padding: 20px;
220
+ background-color: #fff;
221
+ border-radius: 8px;
222
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
223
+ }
224
+ </style>
225
+ </head>
226
+ <body>
227
+ <div class="container">
228
+ <p>You can close this window now.</p>
229
+ <script>
230
+ const urlParams = new URLSearchParams(window.location.search);
231
+ const code = urlParams.get('code');
232
+ 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);
238
+ }
239
+
240
+ // Attempt to close the window
241
+ setTimeout(() => {
242
+ window.close();
243
+ }, 0);
244
+ </script>
245
+ </div>
246
+ </body>
247
+ </html>
248
+ ```
186
249
 
187
- ISC
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.
package/styles.css CHANGED
@@ -1,56 +1,55 @@
1
- .signinwith-button {
2
- display: inline-flex;
3
- align-items: center;
4
- justify-content: center;
5
- padding: 10px 24px;
6
- border: 1px solid #dadce0;
7
- border-radius: 4px;
8
- font-size: 14px;
9
- font-weight: 500;
10
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
11
- cursor: pointer;
12
- text-align: center;
13
- text-decoration: none;
14
- min-width: 220px;
15
- height: 40px;
16
- box-sizing: border-box;
17
- transition: background-color 0.3s ease, border-color 0.3s ease;
18
- color: #3c4043;
19
- background-color: #ffffff;
20
-
21
- &:hover {
22
- background-color: #f8f9fa;
23
- }
24
-
25
- &-meta {
26
- background-color: #1877F2;
27
- color: white;
28
- border-color: #1877F2;
29
-
30
- &:hover {
31
- background-color: #166FE5;
1
+ .signinwith-container {
2
+ display: flex;
3
+ gap: 10px;
4
+ flex-direction: column;
5
+ align-items: stretch;
6
+ padding:10px 0px;
7
+ .signinwith-button {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ padding: 10px 24px;
12
+ border: 1px solid #dadce0;
13
+ border-radius: 4px;
14
+ font-size: 14px;
15
+ font-weight: 500;
16
+ cursor: pointer;
17
+ text-align: center;
18
+ text-decoration: none;
19
+ align-self: stretch;
20
+ box-sizing: border-box;
21
+ transition: background-color 0.3s ease, border-color 0.3s ease;
22
+ color: #3c4043;
23
+ background-color: #ffffff;
24
+
25
+ & svg {
26
+ width:auto;
27
+ height:20px;
28
+ margin-right: 4px;
32
29
  }
33
- }
34
-
35
- &-apple {
36
- background-color: #000000;
37
- color: white;
38
- border-color: #000000;
39
-
30
+
40
31
  &:hover {
41
- background-color: #333333;
32
+ background-color: #f8f9fa;
33
+ }
34
+
35
+ &.signinwith-button-facebook {
36
+ background-color: #1877F2;
37
+ color: white;
38
+ border-color: #0866ff;
39
+
40
+ &:hover {
41
+ background-color: #0866ff;
42
+ }
43
+ }
44
+
45
+ &.signinwith-button-apple {
46
+ background-color: #000000;
47
+ color: white;
48
+ border-color: #000000;
49
+
50
+ &:hover {
51
+ background-color: #333333;
52
+ }
42
53
  }
43
54
  }
44
- }
45
-
46
-
47
- #google-signin-btn > div {
48
- margin: 0 !important;
49
- }
50
-
51
- .signinwith-container {
52
- display: flex;
53
- gap: 1rem;
54
- flex-direction: column;
55
- align-items: center;
56
55
  }