@superfan-app/spotify-auth 0.1.75 → 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, mirroring the iOS SpotifyAuthAuth class.
21
+ * Core Spotify authentication logic for Android.
26
22
  *
27
- * Uses browser-based auth via Spotify's AuthorizationClient.openLoginInBrowser().
28
- * On Android, app-switch is disabled regardless of whether the Spotify app is installed.
29
- * Auth results are delivered via onNewIntent (handled by handleNewIntent()).
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
- * On Android, app-switch is always bypassed. The auth result is delivered
198
- * via onNewIntent, handled by handleNewIntent().
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 scopeArray = scopes.toTypedArray()
235
+ val scopeList = scopes
226
236
 
227
- secureLog("Configuration - ClientID: ${clientId.take(8)}..., RedirectURI: $redirectUri, Scopes: ${scopeArray.size}")
237
+ secureLog("Configuration - ClientID: ${clientId.take(8)}..., RedirectURI: $redirectUri, Scopes: ${scopeList.size}")
228
238
 
229
- if (scopeArray.isEmpty()) {
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
- val builder = AuthorizationRequest.Builder(
243
- clientId,
244
- AuthorizationResponse.Type.CODE,
245
- redirectUri
246
- )
247
- builder.setScopes(scopeArray)
248
-
249
- if (config.showDialog) {
250
- builder.setShowDialog(true)
251
- secureLog("Force-showing login dialog")
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 type: BROWSER (app-switch disabled on Android)")
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, "Response Type: CODE")
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
- // Use browser-based auth on Android (bypasses Spotify app-switch).
286
- // The auth result is delivered via onNewIntent, handled by handleNewIntent().
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 authorization: ${e.message}", e)
291
- throw SpotifyAuthException.AuthenticationFailed("Failed to open Spotify authorization: ${e.message}")
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,135 +305,31 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
306
305
  }
307
306
 
308
307
  /**
309
- * Handle the result from Spotify's AuthorizationClient activity.
310
- * Called by the module's OnActivityResult handler.
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.
311
313
  */
312
- fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
313
- Log.d(TAG, "handleActivityResult called - requestCode=$requestCode, resultCode=$resultCode, hasData=${data != null}")
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
- }
314
+ fun handleNewIntent(intent: Intent) {
315
+ Log.d(TAG, "handleNewIntent called - action=${intent.action}, data=${intent.data}")
359
316
 
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
- )
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.
320
+ val intentData = intent.data
321
+ if (intentData != null) {
322
+ try {
323
+ val configuredUri = Uri.parse(redirectURL)
324
+ if (intentData.scheme != configuredUri.scheme || intentData.host != configuredUri.host) {
325
+ Log.d(TAG, "Ignoring new intent - URI doesn't match redirect (got scheme=${intentData.scheme}, host=${intentData.host})")
326
+ return
422
327
  }
328
+ } catch (e: SpotifyAuthException.MissingConfiguration) {
329
+ Log.w(TAG, "Cannot verify redirect URI in handleNewIntent: ${e.message}")
330
+ // Proceed anyway
423
331
  }
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
332
  }
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.
435
- */
436
- fun handleNewIntent(intent: Intent) {
437
- Log.d(TAG, "handleNewIntent called - action=${intent.action}, data=${intent.data}")
438
333
 
439
334
  if (!isAuthenticating) {
440
335
  Log.d(TAG, "Ignoring new intent - not currently authenticating")
@@ -455,49 +350,33 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
455
350
  return
456
351
  }
457
352
 
458
- try {
459
- val response = AuthorizationClient.getResponse(Activity.RESULT_OK, intent)
460
- Log.d(TAG, "Spotify response type: ${response.type}")
461
-
462
- when (response.type) {
463
- AuthorizationResponse.Type.CODE -> {
464
- val code = response.code
465
- secureLog("Authorization code received, length=${code?.length ?: 0}")
466
- if (code != null) {
467
- exchangeCodeForToken(code)
468
- } else {
469
- Log.e(TAG, "Authorization code was null despite CODE response type")
470
- module?.onAuthorizationError(
471
- SpotifyAuthException.AuthenticationFailed("No authorization code received")
472
- )
473
- }
474
- }
475
- AuthorizationResponse.Type.ERROR -> {
476
- val errorMsg = response.error ?: "Unknown error"
477
- Log.e(TAG, "Spotify authorization error: $errorMsg")
478
- if (errorMsg.contains("access_denied", ignoreCase = true) ||
479
- errorMsg.contains("cancelled", ignoreCase = true)) {
480
- module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
481
- } else {
482
- module?.onAuthorizationError(SpotifyAuthException.AuthorizationError(errorMsg))
483
- }
484
- }
485
- AuthorizationResponse.Type.EMPTY -> {
486
- 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") {
487
371
  module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
488
- }
489
- else -> {
490
- Log.e(TAG, "Unexpected Spotify response type: ${response.type}")
491
- module?.onAuthorizationError(
492
- SpotifyAuthException.AuthenticationFailed("Unexpected response type: ${response.type}")
493
- )
372
+ } else {
373
+ module?.onAuthorizationError(SpotifyAuthException.AuthorizationError(error))
494
374
  }
495
375
  }
496
- } catch (e: Exception) {
497
- Log.e(TAG, "Exception in handleNewIntent: ${e.message}", e)
498
- module?.onAuthorizationError(
499
- SpotifyAuthException.AuthenticationFailed("Error processing auth result: ${e.message}")
500
- )
376
+ else -> {
377
+ Log.w(TAG, "Redirect URI has no code or error parameter - user likely cancelled")
378
+ module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
379
+ }
501
380
  }
502
381
  }
503
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superfan-app/spotify-auth",
3
- "version": "0.1.75",
3
+ "version": "0.1.77",
4
4
  "description": "Spotify OAuth module for Expo",
5
5
  "main": "src/index.tsx",
6
6
  "types": "build/index.d.ts",
@@ -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
  };
@@ -1,6 +1,6 @@
1
1
  // plugin/src/index.ts
2
2
 
3
- import { type ConfigPlugin, createRunOncePlugin, withInfoPlist, withAndroidManifest, withAppBuildGradle, AndroidConfig } from '@expo/config-plugins'
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;