@superfan-app/spotify-auth 0.1.76 → 0.1.77
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.
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package expo.modules.spotifyauth
|
|
2
2
|
|
|
3
|
-
import android.app.Activity
|
|
4
3
|
import android.content.Intent
|
|
5
4
|
import android.content.pm.PackageManager
|
|
6
5
|
import android.net.Uri
|
|
@@ -9,9 +8,6 @@ import android.os.Looper
|
|
|
9
8
|
import android.util.Log
|
|
10
9
|
import androidx.security.crypto.EncryptedSharedPreferences
|
|
11
10
|
import androidx.security.crypto.MasterKey
|
|
12
|
-
import com.spotify.sdk.android.auth.AuthorizationClient
|
|
13
|
-
import com.spotify.sdk.android.auth.AuthorizationRequest
|
|
14
|
-
import com.spotify.sdk.android.auth.AuthorizationResponse
|
|
15
11
|
import expo.modules.kotlin.AppContext
|
|
16
12
|
import org.json.JSONObject
|
|
17
13
|
import java.io.BufferedReader
|
|
@@ -22,11 +18,17 @@ import java.net.URL
|
|
|
22
18
|
import java.util.concurrent.Executors
|
|
23
19
|
|
|
24
20
|
/**
|
|
25
|
-
* Core Spotify authentication logic for Android
|
|
21
|
+
* Core Spotify authentication logic for Android.
|
|
26
22
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
23
|
+
* Opens the Spotify OAuth URL directly in the system browser via Intent.ACTION_VIEW.
|
|
24
|
+
* This avoids using SpotifyAuthorizationActivity (openLoginInBrowser), which conflicts
|
|
25
|
+
* with MainActivity's singleTask launchMode on physical devices: the singleTask mode
|
|
26
|
+
* causes the redirect intent to be routed to MainActivity before SpotifyAuthorizationActivity
|
|
27
|
+
* can call setResult(), resulting in a RESULT_CANCELED that drops the real auth code.
|
|
28
|
+
*
|
|
29
|
+
* By opening the browser directly, the redirect arrives cleanly via onNewIntent on
|
|
30
|
+
* MainActivity, which is handled by handleNewIntent(). This is the same path that
|
|
31
|
+
* works on the emulator.
|
|
30
32
|
*
|
|
31
33
|
* Token exchange and refresh are handled via the backend token swap/refresh URLs,
|
|
32
34
|
* matching the iOS implementation.
|
|
@@ -35,7 +37,6 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
35
37
|
|
|
36
38
|
companion object {
|
|
37
39
|
private const val TAG = "SpotifyAuth"
|
|
38
|
-
private const val REQUEST_CODE = 1337
|
|
39
40
|
private const val ENCRYPTED_PREFS_FILE = "expo_spotify_auth_prefs"
|
|
40
41
|
private const val PREF_REFRESH_TOKEN_KEY = "refresh_token"
|
|
41
42
|
|
|
@@ -194,8 +195,17 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
194
195
|
|
|
195
196
|
/**
|
|
196
197
|
* Initiate the Spotify authorization flow via the system browser.
|
|
197
|
-
*
|
|
198
|
-
*
|
|
198
|
+
*
|
|
199
|
+
* Opens the Spotify OAuth URL directly with Intent.ACTION_VIEW instead of using
|
|
200
|
+
* AuthorizationClient.openLoginInBrowser(), which internally starts
|
|
201
|
+
* SpotifyAuthorizationActivity via startActivityForResult. That approach breaks
|
|
202
|
+
* on physical devices when MainActivity has launchMode="singleTask": the redirect
|
|
203
|
+
* intent is routed to MainActivity (clearing SpotifyAuthorizationActivity from the
|
|
204
|
+
* stack before setResult is called), so onActivityResult gets RESULT_CANCELED and
|
|
205
|
+
* the real auth code arrives via onNewIntent after isAuthenticating has been reset.
|
|
206
|
+
*
|
|
207
|
+
* Opening the browser directly skips SpotifyAuthorizationActivity entirely.
|
|
208
|
+
* The auth result is always delivered via onNewIntent → handleNewIntent().
|
|
199
209
|
*/
|
|
200
210
|
fun initAuth(config: AuthorizeConfig) {
|
|
201
211
|
secureLog("initAuth called with showDialog=${config.showDialog}")
|
|
@@ -222,11 +232,11 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
222
232
|
|
|
223
233
|
val clientId = clientID
|
|
224
234
|
val redirectUri = redirectURL
|
|
225
|
-
val
|
|
235
|
+
val scopeList = scopes
|
|
226
236
|
|
|
227
|
-
secureLog("Configuration - ClientID: ${clientId.take(8)}..., RedirectURI: $redirectUri, Scopes: ${
|
|
237
|
+
secureLog("Configuration - ClientID: ${clientId.take(8)}..., RedirectURI: $redirectUri, Scopes: ${scopeList.size}")
|
|
228
238
|
|
|
229
|
-
if (
|
|
239
|
+
if (scopeList.isEmpty()) {
|
|
230
240
|
Log.e(TAG, "No valid scopes found in configuration")
|
|
231
241
|
throw SpotifyAuthException.InvalidConfiguration("No valid scopes found in configuration")
|
|
232
242
|
}
|
|
@@ -239,32 +249,23 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
239
249
|
isAuthenticating = true
|
|
240
250
|
secureLog("Setting isAuthenticating to true")
|
|
241
251
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// Note: The Android Spotify auth-lib doesn't support a 'campaign' parameter
|
|
255
|
-
// on authorization requests (unlike iOS). The campaign param is ignored on Android.
|
|
256
|
-
|
|
257
|
-
val request = builder.build()
|
|
258
|
-
|
|
259
|
-
secureLog("Opening Spotify authorization in browser")
|
|
252
|
+
// Build the standard Spotify OAuth authorization URL.
|
|
253
|
+
val authUri = Uri.Builder()
|
|
254
|
+
.scheme("https")
|
|
255
|
+
.authority("accounts.spotify.com")
|
|
256
|
+
.path("/authorize")
|
|
257
|
+
.appendQueryParameter("client_id", clientId)
|
|
258
|
+
.appendQueryParameter("response_type", "code")
|
|
259
|
+
.appendQueryParameter("redirect_uri", redirectUri)
|
|
260
|
+
.appendQueryParameter("scope", scopeList.joinToString(" "))
|
|
261
|
+
.apply { if (config.showDialog) appendQueryParameter("show_dialog", "true") }
|
|
262
|
+
.build()
|
|
260
263
|
|
|
261
|
-
// === SPOTIFY AUTH DEBUG ===
|
|
262
264
|
Log.d(TAG, "=== SPOTIFY AUTH DEBUG ===")
|
|
263
|
-
Log.d(TAG, "Auth flow
|
|
265
|
+
Log.d(TAG, "Auth flow: direct browser (Intent.ACTION_VIEW)")
|
|
264
266
|
Log.d(TAG, "Client ID: ${clientId.take(10)}...")
|
|
265
267
|
Log.d(TAG, "Redirect URI: $redirectUri")
|
|
266
|
-
Log.d(TAG, "
|
|
267
|
-
Log.d(TAG, "Scopes: ${scopeArray.joinToString(",")}")
|
|
268
|
+
Log.d(TAG, "Scopes: ${scopeList.joinToString(",")}")
|
|
268
269
|
Log.d(TAG, "Package name: ${appContext.reactContext?.packageName}")
|
|
269
270
|
Log.d(TAG, "Activity: ${activity.javaClass.name}")
|
|
270
271
|
Log.d(TAG, "========================")
|
|
@@ -282,13 +283,11 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
282
283
|
mainHandler.postDelayed(authTimeoutHandler!!, AUTH_TIMEOUT_MS)
|
|
283
284
|
|
|
284
285
|
try {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
AuthorizationClient.openLoginInBrowser(activity, request)
|
|
288
|
-
secureLog("AuthorizationClient.openLoginInBrowser called successfully")
|
|
286
|
+
activity.startActivity(Intent(Intent.ACTION_VIEW, authUri))
|
|
287
|
+
secureLog("Browser opened for Spotify auth")
|
|
289
288
|
} catch (e: Exception) {
|
|
290
|
-
Log.e(TAG, "Failed to open browser
|
|
291
|
-
throw SpotifyAuthException.AuthenticationFailed("Failed to open
|
|
289
|
+
Log.e(TAG, "Failed to open browser: ${e.message}", e)
|
|
290
|
+
throw SpotifyAuthException.AuthenticationFailed("Failed to open browser: ${e.message}")
|
|
292
291
|
}
|
|
293
292
|
|
|
294
293
|
} catch (e: SpotifyAuthException) {
|
|
@@ -306,154 +305,29 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
306
305
|
}
|
|
307
306
|
|
|
308
307
|
/**
|
|
309
|
-
* Handle the
|
|
310
|
-
*
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
// === ENHANCED DEBUG LOGGING FOR ACTIVITY RESULT ===
|
|
316
|
-
if (data != null) {
|
|
317
|
-
Log.d(TAG, "Intent data URI: ${data.data}")
|
|
318
|
-
Log.d(TAG, "Intent action: ${data.action}")
|
|
319
|
-
Log.d(TAG, "Intent extras keys: ${data.extras?.keySet()?.joinToString() ?: "none"}")
|
|
320
|
-
data.extras?.let { extras ->
|
|
321
|
-
for (key in extras.keySet()) {
|
|
322
|
-
val value = extras.get(key)
|
|
323
|
-
if (key.contains("token", ignoreCase = true) ||
|
|
324
|
-
key.contains("code", ignoreCase = true) ||
|
|
325
|
-
key.contains("secret", ignoreCase = true)) {
|
|
326
|
-
Log.d(TAG, " $key: [REDACTED]")
|
|
327
|
-
} else {
|
|
328
|
-
Log.d(TAG, " $key: $value")
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
} else {
|
|
333
|
-
Log.w(TAG, "Intent data is NULL - callback may not have fired correctly")
|
|
334
|
-
Log.w(TAG, "This often indicates an intent filter configuration issue")
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (requestCode != REQUEST_CODE) {
|
|
338
|
-
Log.d(TAG, "Ignoring activity result - wrong request code (expected $REQUEST_CODE, got $requestCode)")
|
|
339
|
-
return
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Cancel the timeout
|
|
343
|
-
authTimeoutHandler?.let {
|
|
344
|
-
mainHandler.removeCallbacks(it)
|
|
345
|
-
secureLog("Auth timeout cancelled")
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (!isAuthenticating) {
|
|
349
|
-
Log.w(TAG, "Received activity result but isAuthenticating was false")
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
isAuthenticating = false
|
|
353
|
-
secureLog("Setting isAuthenticating to false")
|
|
354
|
-
|
|
355
|
-
if (module == null) {
|
|
356
|
-
Log.e(TAG, "CRITICAL: Module is null in handleActivityResult - cannot send events to JS")
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
val response = AuthorizationClient.getResponse(resultCode, data)
|
|
362
|
-
Log.d(TAG, "Spotify response type: ${response.type}")
|
|
363
|
-
|
|
364
|
-
when (response.type) {
|
|
365
|
-
AuthorizationResponse.Type.CODE -> {
|
|
366
|
-
val code = response.code
|
|
367
|
-
secureLog("Authorization code received, length=${code?.length ?: 0}")
|
|
368
|
-
if (code != null) {
|
|
369
|
-
exchangeCodeForToken(code)
|
|
370
|
-
} else {
|
|
371
|
-
Log.e(TAG, "Authorization code was null despite CODE response type")
|
|
372
|
-
module?.onAuthorizationError(
|
|
373
|
-
SpotifyAuthException.AuthenticationFailed("No authorization code received")
|
|
374
|
-
)
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
AuthorizationResponse.Type.ERROR -> {
|
|
378
|
-
val errorMsg = response.error ?: "Unknown error"
|
|
379
|
-
Log.e(TAG, "Spotify authorization error: $errorMsg")
|
|
380
|
-
if (errorMsg.contains("access_denied", ignoreCase = true) ||
|
|
381
|
-
errorMsg.contains("cancelled", ignoreCase = true)) {
|
|
382
|
-
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
383
|
-
} else {
|
|
384
|
-
module?.onAuthorizationError(SpotifyAuthException.AuthorizationError(errorMsg))
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
AuthorizationResponse.Type.EMPTY -> {
|
|
388
|
-
val spotifyInstalled = isSpotifyInstalled()
|
|
389
|
-
if (spotifyInstalled) {
|
|
390
|
-
Log.e(TAG, "")
|
|
391
|
-
Log.e(TAG, "╔══════════════════════════════════════════════════════════════╗")
|
|
392
|
-
Log.e(TAG, "║ SPOTIFY APP-SWITCH AUTH: EMPTY RESPONSE ║")
|
|
393
|
-
Log.e(TAG, "╠══════════════════════════════════════════════════════════════╣")
|
|
394
|
-
Log.e(TAG, "║ Spotify is installed but auth returned empty immediately. ║")
|
|
395
|
-
Log.e(TAG, "║ The auth dialog flashed and dismissed without going to ║")
|
|
396
|
-
Log.e(TAG, "║ Spotify. Common client-side causes: ║")
|
|
397
|
-
Log.e(TAG, "║ ║")
|
|
398
|
-
Log.e(TAG, "║ 1. MainActivity launchMode is singleTask (most likely). ║")
|
|
399
|
-
Log.e(TAG, "║ Change to singleTop in your AndroidManifest.xml. ║")
|
|
400
|
-
Log.e(TAG, "║ ║")
|
|
401
|
-
Log.e(TAG, "║ 2. Redirect URI '${redirectURL.take(30)}...' not registered ║")
|
|
402
|
-
Log.e(TAG, "║ in Spotify Developer Dashboard. ║")
|
|
403
|
-
Log.e(TAG, "║ ║")
|
|
404
|
-
Log.e(TAG, "║ 3. manifestPlaceholders redirectHostName in build.gradle ║")
|
|
405
|
-
Log.e(TAG, "║ does not match the host portion of your redirect URI. ║")
|
|
406
|
-
Log.e(TAG, "║ (Run the Expo config plugin to regenerate.) ║")
|
|
407
|
-
Log.e(TAG, "║ ║")
|
|
408
|
-
Log.e(TAG, "║ 4. Installed Spotify version is too old to support ║")
|
|
409
|
-
Log.e(TAG, "║ app-switch auth. ║")
|
|
410
|
-
Log.e(TAG, "╚══════════════════════════════════════════════════════════════╝")
|
|
411
|
-
Log.e(TAG, "")
|
|
412
|
-
} else {
|
|
413
|
-
Log.w(TAG, "Authorization returned EMPTY - user likely cancelled")
|
|
414
|
-
}
|
|
415
|
-
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
416
|
-
}
|
|
417
|
-
else -> {
|
|
418
|
-
Log.e(TAG, "Unexpected Spotify response type: ${response.type}")
|
|
419
|
-
module?.onAuthorizationError(
|
|
420
|
-
SpotifyAuthException.AuthenticationFailed("Unexpected response type: ${response.type}")
|
|
421
|
-
)
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
} catch (e: Exception) {
|
|
425
|
-
Log.e(TAG, "Exception in handleActivityResult: ${e.message}", e)
|
|
426
|
-
module?.onAuthorizationError(
|
|
427
|
-
SpotifyAuthException.AuthenticationFailed("Error processing auth result: ${e.message}")
|
|
428
|
-
)
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Handle the Spotify auth callback delivered via onNewIntent (browser-based flow).
|
|
434
|
-
* Called by the module's OnNewIntent handler after openLoginInBrowser() completes.
|
|
308
|
+
* Handle the Spotify auth callback delivered via onNewIntent.
|
|
309
|
+
*
|
|
310
|
+
* When the browser redirects to superfan://callback?code=XXX, Android routes the
|
|
311
|
+
* intent to MainActivity (via the intent filter in AndroidManifest), which calls
|
|
312
|
+
* onNewIntent. This method parses the redirect URI directly to extract the auth code.
|
|
435
313
|
*/
|
|
436
314
|
fun handleNewIntent(intent: Intent) {
|
|
437
315
|
Log.d(TAG, "handleNewIntent called - action=${intent.action}, data=${intent.data}")
|
|
438
316
|
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
// processed by AuthorizationClient.getResponse(), which returns EMPTY for
|
|
443
|
-
// non-Spotify intents. That EMPTY result fires a UserCancelled error and resets
|
|
444
|
-
// isAuthenticating=false, so the real Spotify callback is silently dropped when
|
|
445
|
-
// it eventually arrives.
|
|
317
|
+
// Only process intents whose data URI matches our Spotify redirect URI (scheme + host).
|
|
318
|
+
// This guards against push notifications, other deep links, or navigation events
|
|
319
|
+
// arriving while isAuthenticating=true accidentally cancelling the auth flow.
|
|
446
320
|
val intentData = intent.data
|
|
447
321
|
if (intentData != null) {
|
|
448
322
|
try {
|
|
449
323
|
val configuredUri = Uri.parse(redirectURL)
|
|
450
324
|
if (intentData.scheme != configuredUri.scheme || intentData.host != configuredUri.host) {
|
|
451
|
-
Log.d(TAG, "Ignoring new intent -
|
|
325
|
+
Log.d(TAG, "Ignoring new intent - URI doesn't match redirect (got scheme=${intentData.scheme}, host=${intentData.host})")
|
|
452
326
|
return
|
|
453
327
|
}
|
|
454
328
|
} catch (e: SpotifyAuthException.MissingConfiguration) {
|
|
455
329
|
Log.w(TAG, "Cannot verify redirect URI in handleNewIntent: ${e.message}")
|
|
456
|
-
// Proceed
|
|
330
|
+
// Proceed anyway
|
|
457
331
|
}
|
|
458
332
|
}
|
|
459
333
|
|
|
@@ -476,49 +350,33 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
476
350
|
return
|
|
477
351
|
}
|
|
478
352
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
Log.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
val errorMsg = response.error ?: "Unknown error"
|
|
498
|
-
Log.e(TAG, "Spotify authorization error: $errorMsg")
|
|
499
|
-
if (errorMsg.contains("access_denied", ignoreCase = true) ||
|
|
500
|
-
errorMsg.contains("cancelled", ignoreCase = true)) {
|
|
501
|
-
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
502
|
-
} else {
|
|
503
|
-
module?.onAuthorizationError(SpotifyAuthException.AuthorizationError(errorMsg))
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
AuthorizationResponse.Type.EMPTY -> {
|
|
507
|
-
Log.w(TAG, "Browser auth returned EMPTY - user likely cancelled")
|
|
353
|
+
val data = intent.data
|
|
354
|
+
if (data == null) {
|
|
355
|
+
Log.e(TAG, "Redirect intent has no data URI")
|
|
356
|
+
module?.onAuthorizationError(SpotifyAuthException.AuthenticationFailed("No redirect data received"))
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
val code = data.getQueryParameter("code")
|
|
361
|
+
val error = data.getQueryParameter("error")
|
|
362
|
+
|
|
363
|
+
when {
|
|
364
|
+
code != null -> {
|
|
365
|
+
secureLog("Authorization code received")
|
|
366
|
+
exchangeCodeForToken(code)
|
|
367
|
+
}
|
|
368
|
+
error != null -> {
|
|
369
|
+
Log.e(TAG, "Spotify authorization error in redirect: $error")
|
|
370
|
+
if (error == "access_denied") {
|
|
508
371
|
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
Log.e(TAG, "Unexpected Spotify response type: ${response.type}")
|
|
512
|
-
module?.onAuthorizationError(
|
|
513
|
-
SpotifyAuthException.AuthenticationFailed("Unexpected response type: ${response.type}")
|
|
514
|
-
)
|
|
372
|
+
} else {
|
|
373
|
+
module?.onAuthorizationError(SpotifyAuthException.AuthorizationError(error))
|
|
515
374
|
}
|
|
516
375
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
)
|
|
376
|
+
else -> {
|
|
377
|
+
Log.w(TAG, "Redirect URI has no code or error parameter - user likely cancelled")
|
|
378
|
+
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
379
|
+
}
|
|
522
380
|
}
|
|
523
381
|
}
|
|
524
382
|
|
|
@@ -67,21 +67,6 @@ class SpotifyAuthModule : Module() {
|
|
|
67
67
|
spotifyAuth.handleNewIntent(intent)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
OnActivityResult { _, payload ->
|
|
71
|
-
secureLog("OnActivityResult received in module - requestCode=${payload.requestCode}, resultCode=${payload.resultCode}")
|
|
72
|
-
android.util.Log.d("SpotifyAuth", "=== MODULE ACTIVITY RESULT ===")
|
|
73
|
-
android.util.Log.d("SpotifyAuth", "Request code: ${payload.requestCode}")
|
|
74
|
-
android.util.Log.d("SpotifyAuth", "Result code: ${payload.resultCode}")
|
|
75
|
-
android.util.Log.d("SpotifyAuth", "Has data: ${payload.data != null}")
|
|
76
|
-
if (payload.data != null) {
|
|
77
|
-
android.util.Log.d("SpotifyAuth", "Data scheme: ${payload.data?.scheme}")
|
|
78
|
-
android.util.Log.d("SpotifyAuth", "Data host: ${payload.data?.data?.host}")
|
|
79
|
-
android.util.Log.d("SpotifyAuth", "Data path: ${payload.data?.data?.path}")
|
|
80
|
-
}
|
|
81
|
-
android.util.Log.d("SpotifyAuth", "============================")
|
|
82
|
-
spotifyAuth.handleActivityResult(payload.requestCode, payload.resultCode, payload.data)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
70
|
OnDestroy {
|
|
86
71
|
spotifyAuth.cleanup()
|
|
87
72
|
}
|
package/package.json
CHANGED
package/plugin/build/index.js
CHANGED
|
@@ -77,46 +77,6 @@ const withSpotifyConfiguration = (config, props) => {
|
|
|
77
77
|
});
|
|
78
78
|
};
|
|
79
79
|
// region Android config plugins
|
|
80
|
-
/**
|
|
81
|
-
* Injects manifestPlaceholders into the app's build.gradle.
|
|
82
|
-
* Spotify auth lib 3.0.0+ requires redirectSchemeName, redirectHostName, and redirectPathPattern.
|
|
83
|
-
* See: https://github.com/spotify/android-auth
|
|
84
|
-
*/
|
|
85
|
-
const withSpotifyManifestPlaceholders = (config, props) => {
|
|
86
|
-
return (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
|
|
87
|
-
let buildGradle = config.modResults.contents;
|
|
88
|
-
// Escape values for Gradle string literals (escape backslashes and quotes)
|
|
89
|
-
const scheme = String(props.scheme).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
90
|
-
// Split callback into host and optional path — android:host must not contain a slash
|
|
91
|
-
const { host: callbackHost, path: callbackPath } = parseCallback(props.callback);
|
|
92
|
-
const host = String(callbackHost).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
93
|
-
// If the callback has a path component (e.g. "auth/spotify"), prefix the pathPattern
|
|
94
|
-
// so SpotifyAuthorizationActivity's intent filter correctly matches the return URI.
|
|
95
|
-
const pathPattern = callbackPath ? `${callbackPath}.*` : '.*';
|
|
96
|
-
const placeholdersBlock = ` manifestPlaceholders = [
|
|
97
|
-
redirectSchemeName: "${scheme}",
|
|
98
|
-
redirectHostName: "${host}",
|
|
99
|
-
redirectPathPattern: "${pathPattern}"
|
|
100
|
-
]`;
|
|
101
|
-
// Already has all three placeholders (auth lib 3.0.0+ compliant)
|
|
102
|
-
if (buildGradle.includes('redirectPathPattern')) {
|
|
103
|
-
return config;
|
|
104
|
-
}
|
|
105
|
-
// Upgrade: have scheme/host but missing redirectPathPattern (auth lib 3.0.0 breaking change)
|
|
106
|
-
if (buildGradle.includes('redirectSchemeName') && buildGradle.includes('redirectHostName')) {
|
|
107
|
-
buildGradle = buildGradle.replace(/(redirectHostName:\s*"[^"]*")(\s*\n\s*\])/, `$1,\n redirectPathPattern: "${pathPattern}"$2`);
|
|
108
|
-
config.modResults.contents = buildGradle;
|
|
109
|
-
return config;
|
|
110
|
-
}
|
|
111
|
-
// Add manifestPlaceholders to defaultConfig block.
|
|
112
|
-
const defaultConfigRegex = /(defaultConfig\s*\{)/;
|
|
113
|
-
if (defaultConfigRegex.test(buildGradle)) {
|
|
114
|
-
buildGradle = buildGradle.replace(defaultConfigRegex, `$1\n${placeholdersBlock}`);
|
|
115
|
-
}
|
|
116
|
-
config.modResults.contents = buildGradle;
|
|
117
|
-
return config;
|
|
118
|
-
});
|
|
119
|
-
};
|
|
120
80
|
const withSpotifyAndroidManifest = (config, props) => {
|
|
121
81
|
return (0, config_plugins_1.withAndroidManifest)(config, (config) => {
|
|
122
82
|
const mainApplication = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
|
|
@@ -186,7 +146,6 @@ const withSpotifyAuth = (config, props) => {
|
|
|
186
146
|
config = withSpotifyConfiguration(config, props);
|
|
187
147
|
config = withSpotifyURLSchemes(config, props);
|
|
188
148
|
// Apply Android configurations
|
|
189
|
-
config = withSpotifyManifestPlaceholders(config, props);
|
|
190
149
|
config = withSpotifyAndroidManifest(config, props);
|
|
191
150
|
return config;
|
|
192
151
|
};
|
package/plugin/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// plugin/src/index.ts
|
|
2
2
|
|
|
3
|
-
import { type ConfigPlugin, createRunOncePlugin, withInfoPlist, withAndroidManifest,
|
|
3
|
+
import { type ConfigPlugin, createRunOncePlugin, withInfoPlist, withAndroidManifest, AndroidConfig } from '@expo/config-plugins'
|
|
4
4
|
import { SpotifyConfig } from './types.js'
|
|
5
5
|
|
|
6
6
|
const pkg = require('../../package.json');
|
|
@@ -85,59 +85,6 @@ const withSpotifyConfiguration: ConfigPlugin<SpotifyConfig> = (config, props) =>
|
|
|
85
85
|
|
|
86
86
|
// region Android config plugins
|
|
87
87
|
|
|
88
|
-
/**
|
|
89
|
-
* Injects manifestPlaceholders into the app's build.gradle.
|
|
90
|
-
* Spotify auth lib 3.0.0+ requires redirectSchemeName, redirectHostName, and redirectPathPattern.
|
|
91
|
-
* See: https://github.com/spotify/android-auth
|
|
92
|
-
*/
|
|
93
|
-
const withSpotifyManifestPlaceholders: ConfigPlugin<SpotifyConfig> = (config, props) => {
|
|
94
|
-
return withAppBuildGradle(config, (config) => {
|
|
95
|
-
let buildGradle = config.modResults.contents;
|
|
96
|
-
|
|
97
|
-
// Escape values for Gradle string literals (escape backslashes and quotes)
|
|
98
|
-
const scheme = String(props.scheme).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
99
|
-
// Split callback into host and optional path — android:host must not contain a slash
|
|
100
|
-
const { host: callbackHost, path: callbackPath } = parseCallback(props.callback)
|
|
101
|
-
const host = String(callbackHost).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
102
|
-
// If the callback has a path component (e.g. "auth/spotify"), prefix the pathPattern
|
|
103
|
-
// so SpotifyAuthorizationActivity's intent filter correctly matches the return URI.
|
|
104
|
-
const pathPattern = callbackPath ? `${callbackPath}.*` : '.*';
|
|
105
|
-
|
|
106
|
-
const placeholdersBlock = ` manifestPlaceholders = [
|
|
107
|
-
redirectSchemeName: "${scheme}",
|
|
108
|
-
redirectHostName: "${host}",
|
|
109
|
-
redirectPathPattern: "${pathPattern}"
|
|
110
|
-
]`;
|
|
111
|
-
|
|
112
|
-
// Already has all three placeholders (auth lib 3.0.0+ compliant)
|
|
113
|
-
if (buildGradle.includes('redirectPathPattern')) {
|
|
114
|
-
return config;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Upgrade: have scheme/host but missing redirectPathPattern (auth lib 3.0.0 breaking change)
|
|
118
|
-
if (buildGradle.includes('redirectSchemeName') && buildGradle.includes('redirectHostName')) {
|
|
119
|
-
buildGradle = buildGradle.replace(
|
|
120
|
-
/(redirectHostName:\s*"[^"]*")(\s*\n\s*\])/,
|
|
121
|
-
`$1,\n redirectPathPattern: "${pathPattern}"$2`
|
|
122
|
-
);
|
|
123
|
-
config.modResults.contents = buildGradle;
|
|
124
|
-
return config;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Add manifestPlaceholders to defaultConfig block.
|
|
128
|
-
const defaultConfigRegex = /(defaultConfig\s*\{)/;
|
|
129
|
-
if (defaultConfigRegex.test(buildGradle)) {
|
|
130
|
-
buildGradle = buildGradle.replace(
|
|
131
|
-
defaultConfigRegex,
|
|
132
|
-
`$1\n${placeholdersBlock}`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
config.modResults.contents = buildGradle;
|
|
137
|
-
return config;
|
|
138
|
-
});
|
|
139
|
-
};
|
|
140
|
-
|
|
141
88
|
const withSpotifyAndroidManifest: ConfigPlugin<SpotifyConfig> = (config, props) => {
|
|
142
89
|
return withAndroidManifest(config, (config) => {
|
|
143
90
|
const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
|
|
@@ -231,7 +178,6 @@ const withSpotifyAuth: ConfigPlugin<SpotifyConfig> = (config, props) => {
|
|
|
231
178
|
config = withSpotifyURLSchemes(config, props);
|
|
232
179
|
|
|
233
180
|
// Apply Android configurations
|
|
234
|
-
config = withSpotifyManifestPlaceholders(config, props);
|
|
235
181
|
config = withSpotifyAndroidManifest(config, props);
|
|
236
182
|
|
|
237
183
|
return config;
|