@superfan-app/spotify-auth 0.1.73 → 0.1.74

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,7 @@
1
1
  package expo.modules.spotifyauth
2
2
 
3
3
  import android.content.Intent
4
+ import android.content.pm.ActivityInfo
4
5
  import android.content.pm.PackageManager
5
6
  import android.net.Uri
6
7
  import android.os.Handler
@@ -272,9 +273,29 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
272
273
  Log.d(TAG, "Scopes: ${scopeArray.joinToString(",")}")
273
274
  Log.d(TAG, "Package name: ${appContext.reactContext?.packageName}")
274
275
  Log.d(TAG, "Activity: ${activity.javaClass.name}")
275
- Log.d(TAG, "Activity launchMode: ${activity.packageManager.getActivityInfo(activity.componentName, 0).launchMode}")
276
+ val launchMode = activity.packageManager.getActivityInfo(activity.componentName, 0).launchMode
277
+ Log.d(TAG, "Activity launchMode: $launchMode")
276
278
  Log.d(TAG, "========================")
277
279
 
280
+ if (launchMode == ActivityInfo.LAUNCH_SINGLE_TASK || launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
281
+ val modeName = if (launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) "singleTask" else "singleInstance"
282
+ Log.e(TAG, "")
283
+ Log.e(TAG, "╔══════════════════════════════════════════════════════════════╗")
284
+ Log.e(TAG, "║ SPOTIFY APP-SWITCH AUTH WILL LIKELY FAIL ║")
285
+ Log.e(TAG, "╠══════════════════════════════════════════════════════════════╣")
286
+ Log.e(TAG, "║ MainActivity has android:launchMode=\"$modeName\"")
287
+ Log.e(TAG, "║ ║")
288
+ Log.e(TAG, "║ Android 7+ does not properly deliver startActivityForResult ║")
289
+ Log.e(TAG, "║ results to singleTask/singleInstance activities. The Spotify║")
290
+ Log.e(TAG, "║ auth dialog will flash briefly and immediately dismiss. ║")
291
+ Log.e(TAG, "║ ║")
292
+ Log.e(TAG, "║ FIX (client-side): In your app's AndroidManifest.xml, ║")
293
+ Log.e(TAG, "║ change MainActivity's android:launchMode to \"singleTop\" ║")
294
+ Log.e(TAG, "║ or remove the launchMode attribute entirely. ║")
295
+ Log.e(TAG, "╚══════════════════════════════════════════════════════════════╝")
296
+ Log.e(TAG, "")
297
+ }
298
+
278
299
  // Set a timeout to detect if the auth flow doesn't complete
279
300
  authTimeoutHandler = Runnable {
280
301
  if (isAuthenticating) {
@@ -392,7 +413,33 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
392
413
  }
393
414
  }
394
415
  AuthorizationResponse.Type.EMPTY -> {
395
- Log.w(TAG, "Authorization returned EMPTY - user likely cancelled")
416
+ val spotifyInstalled = isSpotifyInstalled()
417
+ if (spotifyInstalled) {
418
+ Log.e(TAG, "")
419
+ Log.e(TAG, "╔══════════════════════════════════════════════════════════════╗")
420
+ Log.e(TAG, "║ SPOTIFY APP-SWITCH AUTH: EMPTY RESPONSE ║")
421
+ Log.e(TAG, "╠══════════════════════════════════════════════════════════════╣")
422
+ Log.e(TAG, "║ Spotify is installed but auth returned empty immediately. ║")
423
+ Log.e(TAG, "║ The auth dialog flashed and dismissed without going to ║")
424
+ Log.e(TAG, "║ Spotify. Common client-side causes: ║")
425
+ Log.e(TAG, "║ ║")
426
+ Log.e(TAG, "║ 1. MainActivity launchMode is singleTask (most likely). ║")
427
+ Log.e(TAG, "║ Change to singleTop in your AndroidManifest.xml. ║")
428
+ Log.e(TAG, "║ ║")
429
+ Log.e(TAG, "║ 2. Redirect URI '${redirectURL.take(30)}...' not registered ║")
430
+ Log.e(TAG, "║ in Spotify Developer Dashboard. ║")
431
+ Log.e(TAG, "║ ║")
432
+ Log.e(TAG, "║ 3. manifestPlaceholders redirectHostName in build.gradle ║")
433
+ Log.e(TAG, "║ does not match the host portion of your redirect URI. ║")
434
+ Log.e(TAG, "║ (Run the Expo config plugin to regenerate.) ║")
435
+ Log.e(TAG, "║ ║")
436
+ Log.e(TAG, "║ 4. Installed Spotify version is too old to support ║")
437
+ Log.e(TAG, "║ app-switch auth. ║")
438
+ Log.e(TAG, "╚══════════════════════════════════════════════════════════════╝")
439
+ Log.e(TAG, "")
440
+ } else {
441
+ Log.w(TAG, "Authorization returned EMPTY - user likely cancelled")
442
+ }
396
443
  module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
397
444
  }
398
445
  else -> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superfan-app/spotify-auth",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
4
4
  "description": "Spotify OAuth module for Expo",
5
5
  "main": "src/index.tsx",
6
6
  "types": "build/index.d.ts",
@@ -3,6 +3,16 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const config_plugins_1 = require("@expo/config-plugins");
5
5
  const pkg = require('../../package.json');
6
+ /**
7
+ * Splits a callback value (e.g. "auth" or "auth/spotify") into its host and optional path components.
8
+ * Android intent filter `android:host` must not include a path segment.
9
+ */
10
+ function parseCallback(callback) {
11
+ const slashIdx = callback.indexOf('/');
12
+ if (slashIdx === -1)
13
+ return { host: callback, path: null };
14
+ return { host: callback.slice(0, slashIdx), path: callback.slice(slashIdx) };
15
+ }
6
16
  function validateSpotifyConfig(config) {
7
17
  if (!config.clientID)
8
18
  throw new Error("Spotify clientID is required");
@@ -77,9 +87,12 @@ const withSpotifyManifestPlaceholders = (config, props) => {
77
87
  let buildGradle = config.modResults.contents;
78
88
  // Escape values for Gradle string literals (escape backslashes and quotes)
79
89
  const scheme = String(props.scheme).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
80
- const host = String(props.callback).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
81
- // ".*" accepts any path - retains previous behavior per Spotify changelog (auth lib 3.0.0)
82
- const pathPattern = '.*';
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}.*` : '.*';
83
96
  const placeholdersBlock = ` manifestPlaceholders = [
84
97
  redirectSchemeName: "${scheme}",
85
98
  redirectHostName: "${host}",
@@ -109,6 +122,8 @@ const withSpotifyAndroidManifest = (config, props) => {
109
122
  const mainApplication = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
110
123
  // Construct the redirect URL from scheme and callback
111
124
  const redirectUrl = `${props.scheme}://${props.callback}`;
125
+ // Split callback into host and optional path for correct intent filter construction
126
+ const { host: callbackHost, path: callbackPath } = parseCallback(props.callback);
112
127
  // Add Spotify configuration as meta-data elements
113
128
  const metaDataEntries = [
114
129
  { name: 'SpotifyClientID', value: props.clientID },
@@ -137,22 +152,24 @@ const withSpotifyAndroidManifest = (config, props) => {
137
152
  mainActivity['intent-filter'] = [];
138
153
  }
139
154
  // Check if we already have a Spotify redirect intent filter
140
- const hasSpotifyIntentFilter = mainActivity['intent-filter'].some((filter) => filter.data?.some((d) => d.$?.['android:scheme'] === props.scheme && d.$?.['android:host'] === props.callback));
155
+ const hasSpotifyIntentFilter = mainActivity['intent-filter'].some((filter) => filter.data?.some((d) => d.$?.['android:scheme'] === props.scheme && d.$?.['android:host'] === callbackHost));
141
156
  if (!hasSpotifyIntentFilter) {
157
+ // Build the data element: android:host must be the hostname only (no path).
158
+ // If the callback has a path component (e.g. "auth/spotify"), add android:pathPrefix.
159
+ const dataAttrs = {
160
+ 'android:scheme': props.scheme,
161
+ 'android:host': callbackHost,
162
+ };
163
+ if (callbackPath) {
164
+ dataAttrs['android:pathPrefix'] = callbackPath;
165
+ }
142
166
  mainActivity['intent-filter'].push({
143
167
  action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
144
168
  category: [
145
169
  { $: { 'android:name': 'android.intent.category.DEFAULT' } },
146
170
  { $: { 'android:name': 'android.intent.category.BROWSABLE' } },
147
171
  ],
148
- data: [
149
- {
150
- $: {
151
- 'android:scheme': props.scheme,
152
- 'android:host': props.callback,
153
- },
154
- },
155
- ],
172
+ data: [{ $: dataAttrs }],
156
173
  });
157
174
  }
158
175
  return config;
@@ -5,6 +5,16 @@ import { SpotifyConfig } from './types.js'
5
5
 
6
6
  const pkg = require('../../package.json');
7
7
 
8
+ /**
9
+ * Splits a callback value (e.g. "auth" or "auth/spotify") into its host and optional path components.
10
+ * Android intent filter `android:host` must not include a path segment.
11
+ */
12
+ function parseCallback(callback: string): { host: string; path: string | null } {
13
+ const slashIdx = callback.indexOf('/')
14
+ if (slashIdx === -1) return { host: callback, path: null }
15
+ return { host: callback.slice(0, slashIdx), path: callback.slice(slashIdx) }
16
+ }
17
+
8
18
  function validateSpotifyConfig(config: SpotifyConfig) {
9
19
  if (!config.clientID) throw new Error("Spotify clientID is required")
10
20
  if (!config.scheme) throw new Error("URL scheme is required")
@@ -86,9 +96,12 @@ const withSpotifyManifestPlaceholders: ConfigPlugin<SpotifyConfig> = (config, pr
86
96
 
87
97
  // Escape values for Gradle string literals (escape backslashes and quotes)
88
98
  const scheme = String(props.scheme).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
89
- const host = String(props.callback).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
90
- // ".*" accepts any path - retains previous behavior per Spotify changelog (auth lib 3.0.0)
91
- const pathPattern = '.*';
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}.*` : '.*';
92
105
 
93
106
  const placeholdersBlock = ` manifestPlaceholders = [
94
107
  redirectSchemeName: "${scheme}",
@@ -131,6 +144,8 @@ const withSpotifyAndroidManifest: ConfigPlugin<SpotifyConfig> = (config, props)
131
144
 
132
145
  // Construct the redirect URL from scheme and callback
133
146
  const redirectUrl = `${props.scheme}://${props.callback}`;
147
+ // Split callback into host and optional path for correct intent filter construction
148
+ const { host: callbackHost, path: callbackPath } = parseCallback(props.callback)
134
149
 
135
150
  // Add Spotify configuration as meta-data elements
136
151
  const metaDataEntries = [
@@ -170,25 +185,28 @@ const withSpotifyAndroidManifest: ConfigPlugin<SpotifyConfig> = (config, props)
170
185
  const hasSpotifyIntentFilter = mainActivity['intent-filter'].some(
171
186
  (filter: any) =>
172
187
  filter.data?.some(
173
- (d: any) => d.$?.['android:scheme'] === props.scheme && d.$?.['android:host'] === props.callback
188
+ (d: any) => d.$?.['android:scheme'] === props.scheme && d.$?.['android:host'] === callbackHost
174
189
  )
175
190
  );
176
191
 
177
192
  if (!hasSpotifyIntentFilter) {
193
+ // Build the data element: android:host must be the hostname only (no path).
194
+ // If the callback has a path component (e.g. "auth/spotify"), add android:pathPrefix.
195
+ const dataAttrs: Record<string, string> = {
196
+ 'android:scheme': props.scheme,
197
+ 'android:host': callbackHost,
198
+ }
199
+ if (callbackPath) {
200
+ dataAttrs['android:pathPrefix'] = callbackPath
201
+ }
202
+
178
203
  mainActivity['intent-filter'].push({
179
204
  action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
180
205
  category: [
181
206
  { $: { 'android:name': 'android.intent.category.DEFAULT' } },
182
207
  { $: { 'android:name': 'android.intent.category.BROWSABLE' } },
183
208
  ],
184
- data: [
185
- {
186
- $: {
187
- 'android:scheme': props.scheme,
188
- 'android:host': props.callback,
189
- },
190
- },
191
- ],
209
+ data: [{ $: dataAttrs }],
192
210
  });
193
211
  }
194
212