@capgo/capacitor-social-login 7.9.6 → 7.11.1
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/README.md +13 -11
- package/android/src/main/java/ee/forgr/capacitor/social/login/AppleProvider.java +326 -5
- package/android/src/main/java/ee/forgr/capacitor/social/login/SocialLoginPlugin.java +6 -1
- package/dist/docs.json +54 -7
- package/dist/esm/apple-provider.d.ts +2 -1
- package/dist/esm/apple-provider.js +18 -9
- package/dist/esm/apple-provider.js.map +1 -1
- package/dist/esm/definitions.d.ts +100 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.js +1 -1
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +19 -10
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +19 -10
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/SocialLoginPlugin/AppleProvider.swift +30 -12
- package/ios/Sources/SocialLoginPlugin/SocialLoginPlugin.swift +4 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -509,7 +509,7 @@ Execute provider-specific calls
|
|
|
509
509
|
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
510
510
|
| **`facebook`** | <code>{ appId: string; clientToken?: string; locale?: string; }</code> |
|
|
511
511
|
| **`google`** | <code>{ iOSClientId?: string; iOSServerClientId?: string; webClientId?: string; mode?: 'online' \| 'offline'; hostedDomain?: string; redirectUrl?: string; }</code> |
|
|
512
|
-
| **`apple`** | <code>{ clientId?: string; redirectUrl?: string; }</code>
|
|
512
|
+
| **`apple`** | <code>{ clientId?: string; redirectUrl?: string; useProperTokenExchange?: boolean; useBroadcastChannel?: boolean; }</code> |
|
|
513
513
|
|
|
514
514
|
|
|
515
515
|
#### FacebookLoginResponse
|
|
@@ -556,11 +556,12 @@ Execute provider-specific calls
|
|
|
556
556
|
|
|
557
557
|
#### AppleProviderResponse
|
|
558
558
|
|
|
559
|
-
| Prop
|
|
560
|
-
|
|
|
561
|
-
| **`accessToken`**
|
|
562
|
-
| **`idToken`**
|
|
563
|
-
| **`profile`**
|
|
559
|
+
| Prop | Type | Description |
|
|
560
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
561
|
+
| **`accessToken`** | <code><a href="#accesstoken">AccessToken</a> \| null</code> | Access token from Apple |
|
|
562
|
+
| **`idToken`** | <code>string \| null</code> | Identity token (JWT) from Apple |
|
|
563
|
+
| **`profile`** | <code>{ user: string; email: string \| null; givenName: string \| null; familyName: string \| null; }</code> | User profile information |
|
|
564
|
+
| **`authorizationCode`** | <code>string</code> | Authorization code for proper token exchange (when useProperTokenExchange is enabled) |
|
|
564
565
|
|
|
565
566
|
|
|
566
567
|
#### FacebookLoginOptions
|
|
@@ -587,11 +588,12 @@ Execute provider-specific calls
|
|
|
587
588
|
|
|
588
589
|
#### AppleProviderOptions
|
|
589
590
|
|
|
590
|
-
| Prop
|
|
591
|
-
|
|
|
592
|
-
| **`scopes`**
|
|
593
|
-
| **`nonce`**
|
|
594
|
-
| **`state`**
|
|
591
|
+
| Prop | Type | Description | Default |
|
|
592
|
+
| ------------------------- | --------------------- | --------------------------------------------- | ------------------ |
|
|
593
|
+
| **`scopes`** | <code>string[]</code> | Scopes | |
|
|
594
|
+
| **`nonce`** | <code>string</code> | Nonce | |
|
|
595
|
+
| **`state`** | <code>string</code> | State | |
|
|
596
|
+
| **`useBroadcastChannel`** | <code>boolean</code> | Use Broadcast Channel for authentication flow | <code>false</code> |
|
|
595
597
|
|
|
596
598
|
|
|
597
599
|
#### isLoggedInOptions
|
|
@@ -17,7 +17,10 @@ import android.view.View;
|
|
|
17
17
|
import android.view.ViewGroup;
|
|
18
18
|
import android.view.Window;
|
|
19
19
|
import android.view.WindowManager;
|
|
20
|
+
import android.webkit.ConsoleMessage;
|
|
21
|
+
import android.webkit.WebChromeClient;
|
|
20
22
|
import android.webkit.WebResourceRequest;
|
|
23
|
+
import android.webkit.WebSettings;
|
|
21
24
|
import android.webkit.WebView;
|
|
22
25
|
import android.webkit.WebViewClient;
|
|
23
26
|
import android.widget.ImageButton;
|
|
@@ -68,6 +71,8 @@ public class AppleProvider implements SocialProvider {
|
|
|
68
71
|
private final String redirectUrl;
|
|
69
72
|
private final Activity activity;
|
|
70
73
|
private final Context context;
|
|
74
|
+
private final boolean useProperTokenExchange;
|
|
75
|
+
private final boolean useBroadcastChannel;
|
|
71
76
|
|
|
72
77
|
private CustomTabsClient customTabsClient;
|
|
73
78
|
private CustomTabsSession currentSession;
|
|
@@ -82,11 +87,20 @@ public class AppleProvider implements SocialProvider {
|
|
|
82
87
|
public void onServiceDisconnected(ComponentName name) {}
|
|
83
88
|
};
|
|
84
89
|
|
|
85
|
-
public AppleProvider(
|
|
90
|
+
public AppleProvider(
|
|
91
|
+
String redirectUrl,
|
|
92
|
+
String clientId,
|
|
93
|
+
Activity activity,
|
|
94
|
+
Context context,
|
|
95
|
+
boolean useProperTokenExchange,
|
|
96
|
+
boolean useBroadcastChannel
|
|
97
|
+
) {
|
|
86
98
|
this.redirectUrl = redirectUrl;
|
|
87
99
|
this.clientId = clientId;
|
|
88
100
|
this.activity = activity;
|
|
89
101
|
this.context = context;
|
|
102
|
+
this.useProperTokenExchange = useProperTokenExchange;
|
|
103
|
+
this.useBroadcastChannel = useBroadcastChannel;
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
public void initialize() {
|
|
@@ -136,6 +150,72 @@ public class AppleProvider implements SocialProvider {
|
|
|
136
150
|
call.reject("Last call is not null");
|
|
137
151
|
}
|
|
138
152
|
|
|
153
|
+
// Check if Broadcast Channel is enabled
|
|
154
|
+
boolean useBroadcastChannel = config.optBoolean("useBroadcastChannel", this.useBroadcastChannel);
|
|
155
|
+
|
|
156
|
+
if (useBroadcastChannel) {
|
|
157
|
+
// Use Broadcast Channel approach - simplified flow
|
|
158
|
+
loginWithBroadcastChannel(call, config);
|
|
159
|
+
} else {
|
|
160
|
+
// Use traditional URL redirect approach
|
|
161
|
+
loginWithRedirect(call, config);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private void loginWithBroadcastChannel(PluginCall call, JSONObject config) {
|
|
166
|
+
String state = UUID.randomUUID().toString();
|
|
167
|
+
|
|
168
|
+
// Extract scopes from config
|
|
169
|
+
String scopes = DEFAULT_SCOPE;
|
|
170
|
+
if (config.has("scopes")) {
|
|
171
|
+
try {
|
|
172
|
+
JSONArray scopesArray = config.getJSONArray("scopes");
|
|
173
|
+
if (scopesArray.length() > 0) {
|
|
174
|
+
scopes = String.join("%20", toStringArray(scopesArray));
|
|
175
|
+
}
|
|
176
|
+
} catch (JSONException e) {
|
|
177
|
+
Log.e(SocialLoginPlugin.LOG_TAG, "Error parsing scopes", e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
String nonce = null;
|
|
182
|
+
if (config.has("nonce")) {
|
|
183
|
+
try {
|
|
184
|
+
nonce = config.getString("nonce");
|
|
185
|
+
} catch (JSONException e) {
|
|
186
|
+
Log.e(SocialLoginPlugin.LOG_TAG, "Error parsing nonce", e);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// For Broadcast Channel, we use a special redirect URI that handles the channel communication
|
|
191
|
+
String broadcastRedirectUri = "https://capacitor-social-login.firebaseapp.com/__/auth/handler";
|
|
192
|
+
|
|
193
|
+
this.appleAuthURLFull =
|
|
194
|
+
AUTHURL +
|
|
195
|
+
"?client_id=" +
|
|
196
|
+
this.clientId +
|
|
197
|
+
"&redirect_uri=" +
|
|
198
|
+
broadcastRedirectUri +
|
|
199
|
+
"&response_type=code&scope=" +
|
|
200
|
+
scopes +
|
|
201
|
+
"&response_mode=form_post&state=" +
|
|
202
|
+
state;
|
|
203
|
+
|
|
204
|
+
if (nonce != null) {
|
|
205
|
+
this.appleAuthURLFull += "&nonce=" + nonce;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (context == null || activity == null) {
|
|
209
|
+
call.reject("Context or Activity is null");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.lastcall = call;
|
|
214
|
+
call.setKeepAlive(true);
|
|
215
|
+
activity.runOnUiThread(() -> setupBroadcastChannelWebview(context, activity, call, appleAuthURLFull));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private void loginWithRedirect(PluginCall call, JSONObject config) {
|
|
139
219
|
String state = UUID.randomUUID().toString();
|
|
140
220
|
|
|
141
221
|
// Extract scopes from config
|
|
@@ -235,6 +315,7 @@ public class AppleProvider implements SocialProvider {
|
|
|
235
315
|
if ("true".equals(success)) {
|
|
236
316
|
String accessToken = uri.getQueryParameter("access_token");
|
|
237
317
|
if (accessToken != null) {
|
|
318
|
+
// We have proper tokens from the backend
|
|
238
319
|
String refreshToken = uri.getQueryParameter("refresh_token");
|
|
239
320
|
String idToken = uri.getQueryParameter("id_token");
|
|
240
321
|
try {
|
|
@@ -244,6 +325,11 @@ public class AppleProvider implements SocialProvider {
|
|
|
244
325
|
result.put("profile", createProfileObject(idToken));
|
|
245
326
|
result.put("idToken", idToken);
|
|
246
327
|
|
|
328
|
+
// For proper token exchange mode, don't include authorization code
|
|
329
|
+
if (!useProperTokenExchange) {
|
|
330
|
+
result.put("authorizationCode", (String) null);
|
|
331
|
+
}
|
|
332
|
+
|
|
247
333
|
JSObject response = new JSObject();
|
|
248
334
|
response.put("provider", "apple");
|
|
249
335
|
response.put("result", result);
|
|
@@ -254,9 +340,18 @@ public class AppleProvider implements SocialProvider {
|
|
|
254
340
|
this.lastcall.reject("Cannot persist state", e);
|
|
255
341
|
}
|
|
256
342
|
} else {
|
|
343
|
+
// We only have authorization code, need to exchange it
|
|
257
344
|
String appleAuthCode = uri.getQueryParameter("code");
|
|
258
345
|
String appleClientSecret = uri.getQueryParameter("client_secret");
|
|
259
|
-
|
|
346
|
+
|
|
347
|
+
if (useProperTokenExchange) {
|
|
348
|
+
// In proper token exchange mode, we should have received proper tokens
|
|
349
|
+
// from the backend. If we only got an auth code, reject the call.
|
|
350
|
+
this.lastcall.reject("Expected proper tokens from backend but received authorization code only");
|
|
351
|
+
} else {
|
|
352
|
+
// Legacy mode: exchange the authorization code for tokens
|
|
353
|
+
requestForAccessToken(appleAuthCode, appleClientSecret);
|
|
354
|
+
}
|
|
260
355
|
}
|
|
261
356
|
} else {
|
|
262
357
|
this.lastcall.reject("We couldn't get the Auth Code");
|
|
@@ -298,9 +393,21 @@ public class AppleProvider implements SocialProvider {
|
|
|
298
393
|
String idToken = jsonObject.getString("id_token");
|
|
299
394
|
|
|
300
395
|
persistState(idToken, refreshToken, accessToken);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
396
|
+
|
|
397
|
+
// Create proper response with all tokens
|
|
398
|
+
JSObject result = new JSObject();
|
|
399
|
+
result.put("accessToken", createAccessTokenObject(accessToken));
|
|
400
|
+
result.put("profile", createProfileObject(idToken));
|
|
401
|
+
result.put("idToken", idToken);
|
|
402
|
+
|
|
403
|
+
// For legacy mode, we don't include authorization code in the response
|
|
404
|
+
// since we've already exchanged it for proper tokens
|
|
405
|
+
|
|
406
|
+
JSObject appleResponse = new JSObject();
|
|
407
|
+
appleResponse.put("provider", "apple");
|
|
408
|
+
appleResponse.put("result", result);
|
|
409
|
+
|
|
410
|
+
AppleProvider.this.lastcall.resolve(appleResponse);
|
|
304
411
|
AppleProvider.this.lastcall = null;
|
|
305
412
|
} catch (Exception e) {
|
|
306
413
|
AppleProvider.this.lastcall.reject("Cannot get access_token", e);
|
|
@@ -348,6 +455,220 @@ public class AppleProvider implements SocialProvider {
|
|
|
348
455
|
builder.build().launchUrl(context, Uri.parse(url));
|
|
349
456
|
}
|
|
350
457
|
|
|
458
|
+
@SuppressLint("SetJavaScriptEnabled")
|
|
459
|
+
private void setupBroadcastChannelWebview(Context context, Activity activity, PluginCall call, String url) {
|
|
460
|
+
// Create a custom WebView with Broadcast Channel support
|
|
461
|
+
Dialog dialog = new Dialog(activity);
|
|
462
|
+
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
|
463
|
+
dialog.setCancelable(true);
|
|
464
|
+
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
|
465
|
+
|
|
466
|
+
WebView webView = new WebView(context);
|
|
467
|
+
webView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
|
468
|
+
|
|
469
|
+
// Enable JavaScript
|
|
470
|
+
WebSettings webSettings = webView.getSettings();
|
|
471
|
+
webSettings.setJavaScriptEnabled(true);
|
|
472
|
+
webSettings.setDomStorageEnabled(true);
|
|
473
|
+
webSettings.setSupportMultipleWindows(false);
|
|
474
|
+
|
|
475
|
+
// Set up Broadcast Channel communication
|
|
476
|
+
webView.addJavascriptInterface(new BroadcastChannelInterface(call), "AndroidBridge");
|
|
477
|
+
|
|
478
|
+
// Set up WebViewClient to handle redirects
|
|
479
|
+
webView.setWebViewClient(
|
|
480
|
+
new WebViewClient() {
|
|
481
|
+
@Override
|
|
482
|
+
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
|
483
|
+
String url = request.getUrl().toString();
|
|
484
|
+
|
|
485
|
+
// Check if this is the Broadcast Channel redirect
|
|
486
|
+
if (url.contains("capacitor-social-login.firebaseapp.com")) {
|
|
487
|
+
// Extract authorization code from URL parameters
|
|
488
|
+
Uri uri = Uri.parse(url);
|
|
489
|
+
String success = uri.getQueryParameter("success");
|
|
490
|
+
|
|
491
|
+
if ("true".equals(success)) {
|
|
492
|
+
String accessToken = uri.getQueryParameter("access_token");
|
|
493
|
+
if (accessToken != null) {
|
|
494
|
+
// We have proper tokens from the backend
|
|
495
|
+
String refreshToken = uri.getQueryParameter("refresh_token");
|
|
496
|
+
String idToken = uri.getQueryParameter("id_token");
|
|
497
|
+
try {
|
|
498
|
+
persistState(idToken, refreshToken, accessToken);
|
|
499
|
+
JSObject result = new JSObject();
|
|
500
|
+
result.put("accessToken", createAccessTokenObject(accessToken));
|
|
501
|
+
result.put("profile", createProfileObject(idToken));
|
|
502
|
+
result.put("idToken", idToken);
|
|
503
|
+
|
|
504
|
+
JSObject response = new JSObject();
|
|
505
|
+
response.put("provider", "apple");
|
|
506
|
+
response.put("result", result);
|
|
507
|
+
|
|
508
|
+
lastcall.resolve(response);
|
|
509
|
+
} catch (JSONException e) {
|
|
510
|
+
Log.e(SocialLoginPlugin.LOG_TAG, "Cannot persist state", e);
|
|
511
|
+
lastcall.reject("Cannot persist state", e);
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
// We only have authorization code, need to handle it
|
|
515
|
+
String appleAuthCode = uri.getQueryParameter("code");
|
|
516
|
+
String appleClientSecret = uri.getQueryParameter("client_secret");
|
|
517
|
+
|
|
518
|
+
if (useProperTokenExchange) {
|
|
519
|
+
// For Broadcast Channel, we can handle the token exchange directly
|
|
520
|
+
// or pass the authorization code back to the client
|
|
521
|
+
JSObject result = new JSObject();
|
|
522
|
+
result.put("authorizationCode", appleAuthCode);
|
|
523
|
+
result.put("idToken", ""); // Will be filled by client-side token exchange
|
|
524
|
+
|
|
525
|
+
JSObject response = new JSObject();
|
|
526
|
+
response.put("provider", "apple");
|
|
527
|
+
response.put("result", result);
|
|
528
|
+
|
|
529
|
+
lastcall.resolve(response);
|
|
530
|
+
} else {
|
|
531
|
+
// Legacy mode: use authorization code as access token
|
|
532
|
+
JSObject result = new JSObject();
|
|
533
|
+
result.put("accessToken", createAccessTokenObject(appleAuthCode));
|
|
534
|
+
result.put("profile", createProfileObject(""));
|
|
535
|
+
result.put("idToken", "");
|
|
536
|
+
|
|
537
|
+
JSObject response = new JSObject();
|
|
538
|
+
response.put("provider", "apple");
|
|
539
|
+
response.put("result", result);
|
|
540
|
+
|
|
541
|
+
lastcall.resolve(response);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
lastcall.reject("Authentication failed");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
dialog.dismiss();
|
|
549
|
+
lastcall = null;
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return super.shouldOverrideUrlLoading(view, request);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
// Inject Broadcast Channel polyfill and setup
|
|
559
|
+
webView.setWebChromeClient(
|
|
560
|
+
new WebChromeClient() {
|
|
561
|
+
@Override
|
|
562
|
+
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
|
|
563
|
+
Log.d("WebView", consoleMessage.message());
|
|
564
|
+
return super.onConsoleMessage(consoleMessage);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
dialog.setContentView(webView);
|
|
570
|
+
|
|
571
|
+
// Set dialog to fullscreen
|
|
572
|
+
WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
|
|
573
|
+
lp.copyFrom(dialog.getWindow().getAttributes());
|
|
574
|
+
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
|
|
575
|
+
lp.height = WindowManager.LayoutParams.MATCH_PARENT;
|
|
576
|
+
dialog.getWindow().setAttributes(lp);
|
|
577
|
+
|
|
578
|
+
// Load the Apple authentication URL
|
|
579
|
+
webView.loadUrl(url);
|
|
580
|
+
|
|
581
|
+
// Inject Broadcast Channel setup after page loads
|
|
582
|
+
webView.setWebViewClient(
|
|
583
|
+
new WebViewClient() {
|
|
584
|
+
@Override
|
|
585
|
+
public void onPageFinished(WebView view, String url) {
|
|
586
|
+
super.onPageFinished(view, url);
|
|
587
|
+
|
|
588
|
+
// Inject Broadcast Channel setup
|
|
589
|
+
String broadcastChannelScript =
|
|
590
|
+
"javascript:" +
|
|
591
|
+
"if (!window.BroadcastChannel) {" +
|
|
592
|
+
" window.BroadcastChannel = function(name) {" +
|
|
593
|
+
" this.name = name;" +
|
|
594
|
+
" this.onmessage = null;" +
|
|
595
|
+
" this.postMessage = function(data) {" +
|
|
596
|
+
" if (window.AndroidBridge) {" +
|
|
597
|
+
" window.AndroidBridge.postMessage(JSON.stringify({channel: this.name, data: data}));" +
|
|
598
|
+
" }" +
|
|
599
|
+
" };" +
|
|
600
|
+
" window.addEventListener('message', (event) => {" +
|
|
601
|
+
" if (this.onmessage) {" +
|
|
602
|
+
" this.onmessage({data: event.data});" +
|
|
603
|
+
" }" +
|
|
604
|
+
" });" +
|
|
605
|
+
" };" +
|
|
606
|
+
"}" +
|
|
607
|
+
"console.log('Broadcast Channel polyfill loaded');";
|
|
608
|
+
|
|
609
|
+
view.evaluateJavascript(broadcastChannelScript, null);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
dialog.show();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// JavaScript interface for Broadcast Channel communication
|
|
618
|
+
private class BroadcastChannelInterface {
|
|
619
|
+
|
|
620
|
+
private final PluginCall call;
|
|
621
|
+
|
|
622
|
+
BroadcastChannelInterface(PluginCall call) {
|
|
623
|
+
this.call = call;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
@android.webkit.JavascriptInterface
|
|
627
|
+
public void postMessage(String message) {
|
|
628
|
+
try {
|
|
629
|
+
JSONObject data = new JSONObject(message);
|
|
630
|
+
String channel = data.getString("channel");
|
|
631
|
+
JSONObject messageData = data.getJSONObject("data");
|
|
632
|
+
|
|
633
|
+
Log.d("BroadcastChannel", "Received message from channel: " + channel);
|
|
634
|
+
Log.d("BroadcastChannel", "Message data: " + messageData.toString());
|
|
635
|
+
|
|
636
|
+
// Handle authentication messages
|
|
637
|
+
if ("auth".equals(channel)) {
|
|
638
|
+
String type = messageData.getString("type");
|
|
639
|
+
if ("success".equals(type)) {
|
|
640
|
+
// Handle successful authentication
|
|
641
|
+
String idToken = messageData.optString("idToken", "");
|
|
642
|
+
String accessToken = messageData.optString("accessToken", "");
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
persistState(idToken, "refresh_token_placeholder", accessToken);
|
|
646
|
+
JSObject result = new JSObject();
|
|
647
|
+
result.put("accessToken", createAccessTokenObject(accessToken));
|
|
648
|
+
result.put("profile", createProfileObject(idToken));
|
|
649
|
+
result.put("idToken", idToken);
|
|
650
|
+
|
|
651
|
+
JSObject response = new JSObject();
|
|
652
|
+
response.put("provider", "apple");
|
|
653
|
+
response.put("result", result);
|
|
654
|
+
|
|
655
|
+
lastcall.resolve(response);
|
|
656
|
+
} catch (JSONException e) {
|
|
657
|
+
Log.e(SocialLoginPlugin.LOG_TAG, "Cannot create response", e);
|
|
658
|
+
lastcall.reject("Cannot create response", e);
|
|
659
|
+
}
|
|
660
|
+
} else if ("error".equals(type)) {
|
|
661
|
+
String error = messageData.optString("error", "Authentication failed");
|
|
662
|
+
lastcall.reject(error);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
} catch (JSONException e) {
|
|
666
|
+
Log.e("BroadcastChannel", "Error parsing message", e);
|
|
667
|
+
lastcall.reject("Error parsing authentication message", e);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
351
672
|
private JSObject createAccessTokenObject(String accessToken) {
|
|
352
673
|
JSObject tokenObject = new JSObject();
|
|
353
674
|
tokenObject.put("token", accessToken);
|
|
@@ -43,11 +43,16 @@ public class SocialLoginPlugin extends Plugin {
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
boolean useProperTokenExchange = apple.has("useProperTokenExchange") ? apple.getBool("useProperTokenExchange") : false;
|
|
47
|
+
boolean useBroadcastChannel = apple.has("useBroadcastChannel") ? apple.getBool("useBroadcastChannel") : false;
|
|
48
|
+
|
|
46
49
|
AppleProvider appleProvider = new AppleProvider(
|
|
47
50
|
androidAppleRedirect,
|
|
48
51
|
androidAppleClientId,
|
|
49
52
|
this.getActivity(),
|
|
50
|
-
this.getContext()
|
|
53
|
+
this.getContext(),
|
|
54
|
+
useProperTokenExchange,
|
|
55
|
+
useBroadcastChannel
|
|
51
56
|
);
|
|
52
57
|
|
|
53
58
|
appleProvider.initialize();
|
package/dist/docs.json
CHANGED
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
"tags": [],
|
|
202
202
|
"docs": "",
|
|
203
203
|
"complexTypes": [],
|
|
204
|
-
"type": "{ clientId?: string | undefined; redirectUrl?: string | undefined; } | undefined"
|
|
204
|
+
"type": "{ clientId?: string | undefined; redirectUrl?: string | undefined; useProperTokenExchange?: boolean | undefined; useBroadcastChannel?: boolean | undefined; } | undefined"
|
|
205
205
|
}
|
|
206
206
|
]
|
|
207
207
|
},
|
|
@@ -380,8 +380,13 @@
|
|
|
380
380
|
"properties": [
|
|
381
381
|
{
|
|
382
382
|
"name": "accessToken",
|
|
383
|
-
"tags": [
|
|
384
|
-
|
|
383
|
+
"tags": [
|
|
384
|
+
{
|
|
385
|
+
"text": "Content depends on `useProperTokenExchange` setting:\n- When `useProperTokenExchange: true`: Real access token from Apple (~1 hour validity)\n- When `useProperTokenExchange: false`: Contains authorization code as token (legacy mode)\nUse `idToken` for user authentication, `accessToken` for API calls when properly exchanged.",
|
|
386
|
+
"name": "description"
|
|
387
|
+
}
|
|
388
|
+
],
|
|
389
|
+
"docs": "Access token from Apple",
|
|
385
390
|
"complexTypes": [
|
|
386
391
|
"AccessToken"
|
|
387
392
|
],
|
|
@@ -389,17 +394,43 @@
|
|
|
389
394
|
},
|
|
390
395
|
{
|
|
391
396
|
"name": "idToken",
|
|
392
|
-
"tags": [
|
|
393
|
-
|
|
397
|
+
"tags": [
|
|
398
|
+
{
|
|
399
|
+
"text": "Always contains the JWT with user identity information including:\n- User ID (sub claim)\n- Email (if user granted permission)\n- Name components (if user granted permission)\n- Email verification status\nThis is the primary token for user authentication and should be verified on your backend.",
|
|
400
|
+
"name": "description"
|
|
401
|
+
}
|
|
402
|
+
],
|
|
403
|
+
"docs": "Identity token (JWT) from Apple",
|
|
394
404
|
"complexTypes": [],
|
|
395
405
|
"type": "string | null"
|
|
396
406
|
},
|
|
397
407
|
{
|
|
398
408
|
"name": "profile",
|
|
399
|
-
"tags": [
|
|
400
|
-
|
|
409
|
+
"tags": [
|
|
410
|
+
{
|
|
411
|
+
"text": "Basic user profile data extracted from the identity token and Apple response:\n- `user`: Apple's user identifier (sub claim from idToken)\n- `email`: User's email address (if permission granted)\n- `givenName`: User's first name (if permission granted)\n- `familyName`: User's last name (if permission granted)",
|
|
412
|
+
"name": "description"
|
|
413
|
+
}
|
|
414
|
+
],
|
|
415
|
+
"docs": "User profile information",
|
|
401
416
|
"complexTypes": [],
|
|
402
417
|
"type": "{ user: string; email: string | null; givenName: string | null; familyName: string | null; }"
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
"name": "authorizationCode",
|
|
421
|
+
"tags": [
|
|
422
|
+
{
|
|
423
|
+
"text": "Only present when `useProperTokenExchange` is `true`. This code should be exchanged\nfor proper access tokens on your backend using Apple's token endpoint. Use this for secure\nserver-side token validation and to obtain refresh tokens.",
|
|
424
|
+
"name": "description"
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
"text": "https ://developer.apple.com/documentation/sign_in_with_apple/tokenresponse",
|
|
428
|
+
"name": "see"
|
|
429
|
+
}
|
|
430
|
+
],
|
|
431
|
+
"docs": "Authorization code for proper token exchange (when useProperTokenExchange is enabled)",
|
|
432
|
+
"complexTypes": [],
|
|
433
|
+
"type": "string | undefined"
|
|
403
434
|
}
|
|
404
435
|
]
|
|
405
436
|
},
|
|
@@ -615,6 +646,22 @@
|
|
|
615
646
|
"docs": "State",
|
|
616
647
|
"complexTypes": [],
|
|
617
648
|
"type": "string | undefined"
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
"name": "useBroadcastChannel",
|
|
652
|
+
"tags": [
|
|
653
|
+
{
|
|
654
|
+
"text": "When enabled, uses Broadcast Channel API for communication instead of URL redirects.\nOnly applicable on platforms that support Broadcast Channel (Android).",
|
|
655
|
+
"name": "description"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
"text": "false",
|
|
659
|
+
"name": "default"
|
|
660
|
+
}
|
|
661
|
+
],
|
|
662
|
+
"docs": "Use Broadcast Channel for authentication flow",
|
|
663
|
+
"complexTypes": [],
|
|
664
|
+
"type": "boolean | undefined"
|
|
618
665
|
}
|
|
619
666
|
]
|
|
620
667
|
},
|
|
@@ -5,7 +5,8 @@ export declare class AppleSocialLogin extends BaseSocialLogin {
|
|
|
5
5
|
private redirectUrl;
|
|
6
6
|
private scriptLoaded;
|
|
7
7
|
private scriptUrl;
|
|
8
|
-
|
|
8
|
+
private useProperTokenExchange;
|
|
9
|
+
initialize(clientId: string | null, redirectUrl: string | null | undefined, useProperTokenExchange?: boolean): Promise<void>;
|
|
9
10
|
login(options: AppleProviderOptions): Promise<LoginResult>;
|
|
10
11
|
logout(): Promise<void>;
|
|
11
12
|
isLoggedIn(): Promise<{
|
|
@@ -6,10 +6,12 @@ export class AppleSocialLogin extends BaseSocialLogin {
|
|
|
6
6
|
this.redirectUrl = null;
|
|
7
7
|
this.scriptLoaded = false;
|
|
8
8
|
this.scriptUrl = 'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js';
|
|
9
|
+
this.useProperTokenExchange = false;
|
|
9
10
|
}
|
|
10
|
-
async initialize(clientId, redirectUrl) {
|
|
11
|
+
async initialize(clientId, redirectUrl, useProperTokenExchange = false) {
|
|
11
12
|
this.clientId = clientId;
|
|
12
13
|
this.redirectUrl = redirectUrl || null;
|
|
14
|
+
this.useProperTokenExchange = useProperTokenExchange;
|
|
13
15
|
if (clientId) {
|
|
14
16
|
await this.loadAppleScript();
|
|
15
17
|
}
|
|
@@ -35,18 +37,25 @@ export class AppleSocialLogin extends BaseSocialLogin {
|
|
|
35
37
|
.signIn()
|
|
36
38
|
.then((res) => {
|
|
37
39
|
var _a, _b, _c, _d, _e;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
let accessToken = null;
|
|
41
|
+
if (this.useProperTokenExchange) {
|
|
42
|
+
// When using proper token exchange, the authorization code should be exchanged
|
|
43
|
+
// for a proper access token on the backend. For now, we set accessToken to null
|
|
44
|
+
// and provide the authorization code in a separate field for backend processing.
|
|
45
|
+
accessToken = null;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Legacy behavior: use authorization code as access token for backward compatibility
|
|
49
|
+
accessToken = {
|
|
50
|
+
token: res.authorization.code || '',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const result = Object.assign({ profile: {
|
|
40
54
|
user: res.user || '',
|
|
41
55
|
email: ((_a = res.user) === null || _a === void 0 ? void 0 : _a.email) || null,
|
|
42
56
|
givenName: ((_c = (_b = res.user) === null || _b === void 0 ? void 0 : _b.name) === null || _c === void 0 ? void 0 : _c.firstName) || null,
|
|
43
57
|
familyName: ((_e = (_d = res.user) === null || _d === void 0 ? void 0 : _d.name) === null || _e === void 0 ? void 0 : _e.lastName) || null,
|
|
44
|
-
},
|
|
45
|
-
accessToken: {
|
|
46
|
-
token: res.authorization.id_token || '',
|
|
47
|
-
},
|
|
48
|
-
idToken: res.authorization.code || null,
|
|
49
|
-
};
|
|
58
|
+
}, accessToken: accessToken, idToken: res.authorization.id_token || null }, (this.useProperTokenExchange && { authorizationCode: res.authorization.code }));
|
|
50
59
|
resolve({ provider: 'apple', result });
|
|
51
60
|
})
|
|
52
61
|
.catch((error) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apple-provider.js","sourceRoot":"","sources":["../../src/apple-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAKzC,MAAM,OAAO,gBAAiB,SAAQ,eAAe;IAArD;;QACU,aAAQ,GAAkB,IAAI,CAAC;QAC/B,gBAAW,GAAkB,IAAI,CAAC;QAClC,iBAAY,GAAG,KAAK,CAAC;QACrB,cAAS,GAAG,sFAAsF,CAAC;
|
|
1
|
+
{"version":3,"file":"apple-provider.js","sourceRoot":"","sources":["../../src/apple-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { BaseSocialLogin } from './base';\nimport type { AppleProviderOptions, AppleProviderResponse, AuthorizationCode, LoginResult } from './definitions';\n\ndeclare const AppleID: any;\n\nexport class AppleSocialLogin extends BaseSocialLogin {\n private clientId: string | null = null;\n private redirectUrl: string | null = null;\n private scriptLoaded = false;\n private scriptUrl = 'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js';\n private useProperTokenExchange = false;\n\n async initialize(\n clientId: string | null,\n redirectUrl: string | null | undefined,\n useProperTokenExchange = false,\n ): Promise<void> {\n this.clientId = clientId;\n this.redirectUrl = redirectUrl || null;\n this.useProperTokenExchange = useProperTokenExchange;\n\n if (clientId) {\n await this.loadAppleScript();\n }\n }\n\n async login(options: AppleProviderOptions): Promise<LoginResult> {\n if (!this.clientId) {\n throw new Error('Apple Client ID not set. Call initialize() first.');\n }\n\n if (!this.scriptLoaded) {\n throw new Error('Apple Sign-In script not loaded.');\n }\n\n return new Promise((resolve, reject) => {\n AppleID.auth.init({\n clientId: this.clientId!,\n scope: options.scopes?.join(' ') || 'name email',\n redirectURI: this.redirectUrl || window.location.href,\n state: options.state,\n nonce: options.nonce,\n usePopup: true,\n });\n\n AppleID.auth\n .signIn()\n .then((res: any) => {\n let accessToken: { token: string } | null = null;\n\n if (this.useProperTokenExchange) {\n // When using proper token exchange, the authorization code should be exchanged\n // for a proper access token on the backend. For now, we set accessToken to null\n // and provide the authorization code in a separate field for backend processing.\n accessToken = null;\n } else {\n // Legacy behavior: use authorization code as access token for backward compatibility\n accessToken = {\n token: res.authorization.code || '',\n };\n }\n\n const result: AppleProviderResponse = {\n profile: {\n user: res.user || '',\n email: res.user?.email || null,\n givenName: res.user?.name?.firstName || null,\n familyName: res.user?.name?.lastName || null,\n },\n accessToken: accessToken,\n idToken: res.authorization.id_token || null,\n // Add authorization code for proper token exchange when flag is enabled\n ...(this.useProperTokenExchange && { authorizationCode: res.authorization.code }),\n };\n resolve({ provider: 'apple', result });\n })\n .catch((error: any) => {\n reject(error);\n });\n });\n }\n\n async logout(): Promise<void> {\n // Apple doesn't provide a logout method for web\n console.log('Apple logout: Session should be managed on the client side');\n }\n\n async isLoggedIn(): Promise<{ isLoggedIn: boolean }> {\n // Apple doesn't provide a method to check login status on web\n console.log('Apple login status should be managed on the client side');\n return { isLoggedIn: false };\n }\n\n async getAuthorizationCode(): Promise<AuthorizationCode> {\n // Apple authorization code should be obtained during login\n console.log('Apple authorization code should be stored during login');\n throw new Error('Apple authorization code not available');\n }\n\n async refresh(): Promise<void> {\n // Apple doesn't provide a refresh method for web\n console.log('Apple refresh not available on web');\n }\n\n private async loadAppleScript(): Promise<void> {\n if (this.scriptLoaded) return;\n\n return this.loadScript(this.scriptUrl).then(() => {\n this.scriptLoaded = true;\n });\n }\n}\n"]}
|