@capgo/capacitor-social-login 7.15.2 → 7.17.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/README.md +55 -18
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/ee/forgr/capacitor/social/login/AppleProvider.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor/social/login/SocialLoginPlugin.java +32 -1
- package/android/src/main/java/ee/forgr/capacitor/social/login/TwitterLoginActivity.java +93 -0
- package/android/src/main/java/ee/forgr/capacitor/social/login/TwitterProvider.java +510 -0
- package/dist/docs.json +184 -8
- package/dist/esm/definitions.d.ts +83 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/google-provider.d.ts +3 -1
- package/dist/esm/google-provider.js +25 -3
- package/dist/esm/google-provider.js.map +1 -1
- package/dist/esm/twitter-provider.d.ts +36 -0
- package/dist/esm/twitter-provider.js +346 -0
- package/dist/esm/twitter-provider.js.map +1 -0
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +59 -8
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +428 -11
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +428 -11
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/SocialLoginPlugin/SocialLoginPlugin.swift +93 -1
- package/ios/Sources/SocialLoginPlugin/TwitterProvider.swift +381 -0
- package/package.json +1 -1
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
package ee.forgr.capacitor.social.login;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.content.Intent;
|
|
6
|
+
import android.content.SharedPreferences;
|
|
7
|
+
import android.net.Uri;
|
|
8
|
+
import android.text.TextUtils;
|
|
9
|
+
import android.util.Base64;
|
|
10
|
+
import android.util.Log;
|
|
11
|
+
import com.getcapacitor.JSArray;
|
|
12
|
+
import com.getcapacitor.JSObject;
|
|
13
|
+
import com.getcapacitor.PluginCall;
|
|
14
|
+
import ee.forgr.capacitor.social.login.helpers.SocialProvider;
|
|
15
|
+
import java.io.IOException;
|
|
16
|
+
import java.nio.charset.StandardCharsets;
|
|
17
|
+
import java.security.MessageDigest;
|
|
18
|
+
import java.security.NoSuchAlgorithmException;
|
|
19
|
+
import java.security.SecureRandom;
|
|
20
|
+
import java.util.ArrayList;
|
|
21
|
+
import java.util.Arrays;
|
|
22
|
+
import java.util.Collections;
|
|
23
|
+
import java.util.List;
|
|
24
|
+
import java.util.UUID;
|
|
25
|
+
import java.util.concurrent.TimeUnit;
|
|
26
|
+
import okhttp3.Call;
|
|
27
|
+
import okhttp3.Callback;
|
|
28
|
+
import okhttp3.FormBody;
|
|
29
|
+
import okhttp3.OkHttpClient;
|
|
30
|
+
import okhttp3.Request;
|
|
31
|
+
import okhttp3.Response;
|
|
32
|
+
import org.json.JSONArray;
|
|
33
|
+
import org.json.JSONException;
|
|
34
|
+
import org.json.JSONObject;
|
|
35
|
+
|
|
36
|
+
public class TwitterProvider implements SocialProvider {
|
|
37
|
+
|
|
38
|
+
public static final int REQUEST_CODE = 9401;
|
|
39
|
+
private static final String LOG_TAG = "TwitterProvider";
|
|
40
|
+
private static final String TOKEN_ENDPOINT = "https://api.x.com/2/oauth2/token";
|
|
41
|
+
private static final String PROFILE_ENDPOINT = "https://api.x.com/2/users/me";
|
|
42
|
+
private static final String PREFS_NAME = "CapgoTwitterProviderPrefs";
|
|
43
|
+
private static final String PREFS_KEY = "TwitterTokens";
|
|
44
|
+
|
|
45
|
+
private final Activity activity;
|
|
46
|
+
private final Context context;
|
|
47
|
+
private final OkHttpClient httpClient;
|
|
48
|
+
|
|
49
|
+
private String clientId;
|
|
50
|
+
private String redirectUri;
|
|
51
|
+
private List<String> defaultScopes = Arrays.asList("tweet.read", "users.read");
|
|
52
|
+
private boolean forceLogin = false;
|
|
53
|
+
private String audience;
|
|
54
|
+
|
|
55
|
+
private PluginCall pendingCall;
|
|
56
|
+
private TwitterPendingState pendingState;
|
|
57
|
+
|
|
58
|
+
private static class TwitterPendingState {
|
|
59
|
+
|
|
60
|
+
final String state;
|
|
61
|
+
final String codeVerifier;
|
|
62
|
+
final String redirectUri;
|
|
63
|
+
final List<String> scopes;
|
|
64
|
+
|
|
65
|
+
TwitterPendingState(String state, String codeVerifier, String redirectUri, List<String> scopes) {
|
|
66
|
+
this.state = state;
|
|
67
|
+
this.codeVerifier = codeVerifier;
|
|
68
|
+
this.redirectUri = redirectUri;
|
|
69
|
+
this.scopes = scopes;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public TwitterProvider(Activity activity, Context context) {
|
|
74
|
+
this.activity = activity;
|
|
75
|
+
this.context = context;
|
|
76
|
+
this.httpClient = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).build();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public void initialize(JSONObject config) throws JSONException {
|
|
80
|
+
this.clientId = config.getString("clientId");
|
|
81
|
+
this.redirectUri = config.getString("redirectUrl");
|
|
82
|
+
if (config.has("defaultScopes")) {
|
|
83
|
+
JSONArray scopesArray = config.getJSONArray("defaultScopes");
|
|
84
|
+
this.defaultScopes = jsonArrayToList(scopesArray);
|
|
85
|
+
}
|
|
86
|
+
this.forceLogin = config.optBoolean("forceLogin", false);
|
|
87
|
+
this.audience = config.optString("audience", null);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Override
|
|
91
|
+
public void login(PluginCall call, JSONObject config) {
|
|
92
|
+
if (clientId == null || redirectUri == null) {
|
|
93
|
+
call.reject("Twitter provider is not initialized. Call initialize() first.");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (pendingCall != null) {
|
|
97
|
+
call.reject("Another Twitter login is already running.");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
List<String> scopes = defaultScopes;
|
|
102
|
+
if (config != null && config.has("scopes")) {
|
|
103
|
+
try {
|
|
104
|
+
scopes = jsonArrayToList(config.getJSONArray("scopes"));
|
|
105
|
+
} catch (JSONException e) {
|
|
106
|
+
call.reject("Invalid scopes format", e);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
boolean forceLoginOverride = config != null && config.has("forceLogin") ? config.optBoolean("forceLogin", forceLogin) : forceLogin;
|
|
111
|
+
String redirect = redirectUri;
|
|
112
|
+
if (config != null && config.has("redirectUrl")) {
|
|
113
|
+
redirect = config.optString("redirectUrl", redirectUri);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
String state = config != null && config.has("state")
|
|
117
|
+
? config.optString("state", UUID.randomUUID().toString())
|
|
118
|
+
: UUID.randomUUID().toString();
|
|
119
|
+
String codeVerifier = generateCodeVerifier();
|
|
120
|
+
String codeChallenge;
|
|
121
|
+
try {
|
|
122
|
+
codeChallenge = generateCodeChallenge(codeVerifier);
|
|
123
|
+
} catch (NoSuchAlgorithmException e) {
|
|
124
|
+
call.reject("Unable to generate code challenge", e);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pendingState = new TwitterPendingState(state, codeVerifier, redirect, scopes);
|
|
129
|
+
pendingCall = call;
|
|
130
|
+
|
|
131
|
+
Uri.Builder builder = Uri.parse("https://x.com/i/oauth2/authorize")
|
|
132
|
+
.buildUpon()
|
|
133
|
+
.appendQueryParameter("response_type", "code")
|
|
134
|
+
.appendQueryParameter("client_id", clientId)
|
|
135
|
+
.appendQueryParameter("redirect_uri", redirect)
|
|
136
|
+
.appendQueryParameter("scope", TextUtils.join(" ", scopes))
|
|
137
|
+
.appendQueryParameter("state", state)
|
|
138
|
+
.appendQueryParameter("code_challenge", codeChallenge)
|
|
139
|
+
.appendQueryParameter("code_challenge_method", "S256");
|
|
140
|
+
if (forceLoginOverride) {
|
|
141
|
+
builder.appendQueryParameter("force_login", "true");
|
|
142
|
+
}
|
|
143
|
+
if (audience != null && !audience.isEmpty()) {
|
|
144
|
+
builder.appendQueryParameter("audience", audience);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Intent intent = new Intent(activity, TwitterLoginActivity.class);
|
|
148
|
+
intent.putExtra(TwitterLoginActivity.EXTRA_AUTH_URL, builder.build().toString());
|
|
149
|
+
intent.putExtra(TwitterLoginActivity.EXTRA_REDIRECT_URL, redirect);
|
|
150
|
+
activity.startActivityForResult(intent, REQUEST_CODE);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Override
|
|
154
|
+
public void logout(PluginCall call) {
|
|
155
|
+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit().remove(PREFS_KEY).apply();
|
|
156
|
+
call.resolve();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Override
|
|
160
|
+
public void getAuthorizationCode(PluginCall call) {
|
|
161
|
+
TwitterStoredTokens tokens = loadStoredTokens();
|
|
162
|
+
if (tokens == null) {
|
|
163
|
+
call.reject("Twitter access token not available");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
JSObject response = new JSObject();
|
|
167
|
+
response.put("accessToken", tokens.accessToken);
|
|
168
|
+
if (tokens.refreshToken != null) {
|
|
169
|
+
response.put("refreshToken", tokens.refreshToken);
|
|
170
|
+
}
|
|
171
|
+
response.put("tokenType", tokens.tokenType);
|
|
172
|
+
call.resolve(response);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@Override
|
|
176
|
+
public void isLoggedIn(PluginCall call) {
|
|
177
|
+
TwitterStoredTokens tokens = loadStoredTokens();
|
|
178
|
+
boolean isValid = tokens != null && tokens.expiresAt > System.currentTimeMillis();
|
|
179
|
+
if (!isValid) {
|
|
180
|
+
call.resolve(new JSObject().put("isLoggedIn", false));
|
|
181
|
+
} else {
|
|
182
|
+
call.resolve(new JSObject().put("isLoggedIn", true));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Override
|
|
187
|
+
public void refresh(PluginCall call) {
|
|
188
|
+
TwitterStoredTokens tokens = loadStoredTokens();
|
|
189
|
+
if (tokens == null || tokens.refreshToken == null) {
|
|
190
|
+
call.reject("Twitter refresh token is not available. Make sure offline.access scope is granted.");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
refreshWithToken(call, tokens.refreshToken);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
|
|
197
|
+
if (requestCode != REQUEST_CODE) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (pendingCall == null || pendingState == null) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (resultCode != Activity.RESULT_OK) {
|
|
205
|
+
String error = data != null ? data.getStringExtra("error") : "User cancelled";
|
|
206
|
+
pendingCall.reject(error != null ? error : "User cancelled");
|
|
207
|
+
cleanupPending();
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
String returnedState = data.getStringExtra("state");
|
|
212
|
+
if (returnedState == null || !returnedState.equals(pendingState.state)) {
|
|
213
|
+
pendingCall.reject("State mismatch during Twitter login");
|
|
214
|
+
cleanupPending();
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
String error = data.getStringExtra("error");
|
|
219
|
+
if (error != null) {
|
|
220
|
+
String description = data.getStringExtra("error_description");
|
|
221
|
+
pendingCall.reject(description != null ? description : error);
|
|
222
|
+
cleanupPending();
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
String code = data.getStringExtra("code");
|
|
227
|
+
if (code == null) {
|
|
228
|
+
pendingCall.reject("Authorization code missing");
|
|
229
|
+
cleanupPending();
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
exchangeAuthorizationCode(code);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private void exchangeAuthorizationCode(String code) {
|
|
238
|
+
if (pendingState == null) {
|
|
239
|
+
if (pendingCall != null) {
|
|
240
|
+
pendingCall.reject("Internal error: missing pending state");
|
|
241
|
+
cleanupPending();
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
FormBody body = new FormBody.Builder()
|
|
247
|
+
.add("grant_type", "authorization_code")
|
|
248
|
+
.add("client_id", clientId)
|
|
249
|
+
.add("code", code)
|
|
250
|
+
.add("redirect_uri", pendingState.redirectUri)
|
|
251
|
+
.add("code_verifier", pendingState.codeVerifier)
|
|
252
|
+
.build();
|
|
253
|
+
|
|
254
|
+
Request request = new Request.Builder().url(TOKEN_ENDPOINT).post(body).build();
|
|
255
|
+
httpClient
|
|
256
|
+
.newCall(request)
|
|
257
|
+
.enqueue(
|
|
258
|
+
new Callback() {
|
|
259
|
+
@Override
|
|
260
|
+
public void onFailure(Call call, IOException e) {
|
|
261
|
+
if (pendingCall != null) {
|
|
262
|
+
pendingCall.reject("Twitter token exchange failed", e);
|
|
263
|
+
}
|
|
264
|
+
cleanupPending();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@Override
|
|
268
|
+
public void onResponse(Call call, Response response) throws IOException {
|
|
269
|
+
if (!response.isSuccessful()) {
|
|
270
|
+
String errorBody = response.body() != null ? response.body().string() : "";
|
|
271
|
+
if (pendingCall != null) {
|
|
272
|
+
pendingCall.reject("Twitter token exchange failed: " + errorBody);
|
|
273
|
+
}
|
|
274
|
+
cleanupPending();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
String responseBody = response.body() != null ? response.body().string() : "";
|
|
278
|
+
try {
|
|
279
|
+
JSONObject tokenPayload = new JSONObject(responseBody);
|
|
280
|
+
handleTokenSuccess(tokenPayload);
|
|
281
|
+
} catch (JSONException e) {
|
|
282
|
+
if (pendingCall != null) {
|
|
283
|
+
pendingCall.reject("Failed to parse Twitter token response", e);
|
|
284
|
+
}
|
|
285
|
+
cleanupPending();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private void refreshWithToken(final PluginCall pluginCall, String refreshToken) {
|
|
293
|
+
FormBody body = new FormBody.Builder()
|
|
294
|
+
.add("grant_type", "refresh_token")
|
|
295
|
+
.add("refresh_token", refreshToken)
|
|
296
|
+
.add("client_id", clientId)
|
|
297
|
+
.build();
|
|
298
|
+
|
|
299
|
+
Request request = new Request.Builder().url(TOKEN_ENDPOINT).post(body).build();
|
|
300
|
+
httpClient
|
|
301
|
+
.newCall(request)
|
|
302
|
+
.enqueue(
|
|
303
|
+
new Callback() {
|
|
304
|
+
@Override
|
|
305
|
+
public void onFailure(Call call, IOException e) {
|
|
306
|
+
pluginCall.reject("Twitter refresh failed", e);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@Override
|
|
310
|
+
public void onResponse(Call call, Response response) throws IOException {
|
|
311
|
+
if (!response.isSuccessful()) {
|
|
312
|
+
String errorBody = response.body() != null ? response.body().string() : "";
|
|
313
|
+
pluginCall.reject("Twitter refresh failed: " + errorBody);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
String responseBody = response.body() != null ? response.body().string() : "";
|
|
317
|
+
try {
|
|
318
|
+
JSONObject tokenPayload = new JSONObject(responseBody);
|
|
319
|
+
handleTokenSuccess(tokenPayload, pluginCall);
|
|
320
|
+
} catch (JSONException e) {
|
|
321
|
+
pluginCall.reject("Failed to parse Twitter refresh response", e);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private void handleTokenSuccess(JSONObject tokenPayload) throws JSONException {
|
|
329
|
+
handleTokenSuccess(tokenPayload, pendingCall);
|
|
330
|
+
cleanupPending();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private void handleTokenSuccess(JSONObject tokenPayload, PluginCall call) throws JSONException {
|
|
334
|
+
if (call == null) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
final String accessToken = tokenPayload.getString("access_token");
|
|
338
|
+
final String tokenType = tokenPayload.optString("token_type", "bearer");
|
|
339
|
+
final int expiresIn = tokenPayload.optInt("expires_in", 0);
|
|
340
|
+
final String refreshToken = tokenPayload.optString("refresh_token", null);
|
|
341
|
+
final String scopeRaw = tokenPayload.optString("scope", "");
|
|
342
|
+
final List<String> scopes = scopeRaw.isEmpty() ? Collections.emptyList() : Arrays.asList(scopeRaw.split(" "));
|
|
343
|
+
|
|
344
|
+
fetchProfile(
|
|
345
|
+
accessToken,
|
|
346
|
+
new ProfileCallback() {
|
|
347
|
+
@Override
|
|
348
|
+
public void onSuccess(JSONObject profile) {
|
|
349
|
+
persistTokens(accessToken, refreshToken, tokenType, expiresIn, profile);
|
|
350
|
+
JSObject accessTokenObject = new JSObject();
|
|
351
|
+
accessTokenObject.put("token", accessToken);
|
|
352
|
+
accessTokenObject.put("tokenType", tokenType);
|
|
353
|
+
accessTokenObject.put("expiresIn", expiresIn);
|
|
354
|
+
if (refreshToken != null) {
|
|
355
|
+
accessTokenObject.put("refreshToken", refreshToken);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
JSObject result = new JSObject();
|
|
359
|
+
result.put("accessToken", accessTokenObject);
|
|
360
|
+
result.put("profile", profile);
|
|
361
|
+
result.put("scope", new JSArray(scopes));
|
|
362
|
+
result.put("tokenType", tokenType);
|
|
363
|
+
result.put("expiresIn", expiresIn);
|
|
364
|
+
|
|
365
|
+
JSObject response = new JSObject();
|
|
366
|
+
response.put("provider", "twitter");
|
|
367
|
+
response.put("result", result);
|
|
368
|
+
call.resolve(response);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@Override
|
|
372
|
+
public void onError(String message) {
|
|
373
|
+
call.reject(message);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private interface ProfileCallback {
|
|
380
|
+
void onSuccess(JSONObject profile);
|
|
381
|
+
void onError(String message);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private void fetchProfile(String accessToken, ProfileCallback callback) {
|
|
385
|
+
Uri uri = Uri.parse(PROFILE_ENDPOINT)
|
|
386
|
+
.buildUpon()
|
|
387
|
+
.appendQueryParameter("user.fields", "profile_image_url,verified,name,username")
|
|
388
|
+
.build();
|
|
389
|
+
Request request = new Request.Builder().url(uri.toString()).addHeader("Authorization", "Bearer " + accessToken).build();
|
|
390
|
+
|
|
391
|
+
httpClient
|
|
392
|
+
.newCall(request)
|
|
393
|
+
.enqueue(
|
|
394
|
+
new Callback() {
|
|
395
|
+
@Override
|
|
396
|
+
public void onFailure(Call call, IOException e) {
|
|
397
|
+
callback.onError("Failed to fetch Twitter profile: " + e.getMessage());
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
@Override
|
|
401
|
+
public void onResponse(Call call, Response response) throws IOException {
|
|
402
|
+
if (!response.isSuccessful()) {
|
|
403
|
+
String errorBody = response.body() != null ? response.body().string() : "";
|
|
404
|
+
callback.onError("Failed to fetch Twitter profile: " + errorBody);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
String responseBody = response.body() != null ? response.body().string() : "";
|
|
408
|
+
try {
|
|
409
|
+
JSONObject payload = new JSONObject(responseBody);
|
|
410
|
+
JSONObject data = payload.getJSONObject("data");
|
|
411
|
+
JSONObject profile = new JSONObject();
|
|
412
|
+
profile.put("id", data.optString("id"));
|
|
413
|
+
profile.put("username", data.optString("username"));
|
|
414
|
+
profile.put("name", data.optString("name"));
|
|
415
|
+
profile.put("profileImageUrl", data.optString("profile_image_url", ""));
|
|
416
|
+
profile.put("verified", data.optBoolean("verified", false));
|
|
417
|
+
if (data.has("email")) {
|
|
418
|
+
profile.put("email", data.optString("email", null));
|
|
419
|
+
} else {
|
|
420
|
+
profile.put("email", JSONObject.NULL);
|
|
421
|
+
}
|
|
422
|
+
callback.onSuccess(profile);
|
|
423
|
+
} catch (JSONException e) {
|
|
424
|
+
callback.onError("Failed to parse Twitter profile response");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private void persistTokens(String accessToken, String refreshToken, String tokenType, int expiresIn, JSONObject profile) {
|
|
432
|
+
try {
|
|
433
|
+
JSONObject stored = new JSONObject();
|
|
434
|
+
stored.put("accessToken", accessToken);
|
|
435
|
+
stored.put("tokenType", tokenType);
|
|
436
|
+
stored.put("expiresAt", System.currentTimeMillis() + (long) expiresIn * 1000L);
|
|
437
|
+
stored.put("refreshToken", refreshToken);
|
|
438
|
+
stored.put("userId", profile.optString("id"));
|
|
439
|
+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
440
|
+
prefs.edit().putString(PREFS_KEY, stored.toString()).apply();
|
|
441
|
+
} catch (JSONException e) {
|
|
442
|
+
Log.w(LOG_TAG, "Failed to persist Twitter tokens", e);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private TwitterStoredTokens loadStoredTokens() {
|
|
447
|
+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
448
|
+
String raw = prefs.getString(PREFS_KEY, null);
|
|
449
|
+
if (raw == null || raw.isEmpty()) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
JSONObject object = new JSONObject(raw);
|
|
454
|
+
String accessToken = object.optString("accessToken", null);
|
|
455
|
+
if (accessToken == null || accessToken.isEmpty()) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
return new TwitterStoredTokens(
|
|
459
|
+
accessToken,
|
|
460
|
+
object.optString("refreshToken", null),
|
|
461
|
+
object.optLong("expiresAt", 0L),
|
|
462
|
+
object.optString("tokenType", "bearer")
|
|
463
|
+
);
|
|
464
|
+
} catch (JSONException e) {
|
|
465
|
+
Log.w(LOG_TAG, "Failed to parse stored Twitter tokens", e);
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private void cleanupPending() {
|
|
471
|
+
pendingCall = null;
|
|
472
|
+
pendingState = null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private static List<String> jsonArrayToList(JSONArray array) throws JSONException {
|
|
476
|
+
List<String> list = new ArrayList<>();
|
|
477
|
+
for (int i = 0; i < array.length(); i++) {
|
|
478
|
+
list.add(array.getString(i));
|
|
479
|
+
}
|
|
480
|
+
return list;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private static String generateCodeVerifier() {
|
|
484
|
+
SecureRandom secureRandom = new SecureRandom();
|
|
485
|
+
byte[] code = new byte[64];
|
|
486
|
+
secureRandom.nextBytes(code);
|
|
487
|
+
return Base64.encodeToString(code, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private static String generateCodeChallenge(String verifier) throws NoSuchAlgorithmException {
|
|
491
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
492
|
+
byte[] hash = digest.digest(verifier.getBytes(StandardCharsets.US_ASCII));
|
|
493
|
+
return Base64.encodeToString(hash, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private static class TwitterStoredTokens {
|
|
497
|
+
|
|
498
|
+
final String accessToken;
|
|
499
|
+
final String refreshToken;
|
|
500
|
+
final long expiresAt;
|
|
501
|
+
final String tokenType;
|
|
502
|
+
|
|
503
|
+
TwitterStoredTokens(String accessToken, String refreshToken, long expiresAt, String tokenType) {
|
|
504
|
+
this.accessToken = accessToken;
|
|
505
|
+
this.refreshToken = refreshToken;
|
|
506
|
+
this.expiresAt = expiresAt;
|
|
507
|
+
this.tokenType = tokenType;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|