@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
|
-
|
|
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
|
-
|
|
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
package/plugin/build/index.js
CHANGED
|
@@ -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
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
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'] ===
|
|
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;
|
package/plugin/src/index.ts
CHANGED
|
@@ -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
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
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'] ===
|
|
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
|
|