Open hspinks opened 5 years ago
Does this question refer to the Streaming SDK instead of this one? Please note that the Streaming SDK is deprecated and no longer supported.
@kkarayannis no, this is for this remote SDK... I'm trying to use the SPTSessionManager and SPTSession classes in this SDK
Looking to solve the same problem. There seems to be very little tools to help with sessions persistence. I might just have to scrap using the whole session manager and manually deal with the token swapping.
@hspinks did you find any solution for this problem? It is a shame that such a big company like Spotify can't provide solution for such a trivial thing.
@kkarayannis any update?
I would also be interested in the reply. Our app will potentially run for several hours with playing music while the user is away from the iPhone, I can't imagine that every 60 minutes the playback will be stopped and the user needs to do the app switch manually in order for the music to continue the playback. That will be a showstopper for us.
@kkarayannis please let us know about the flow
Besides that the Spotify iOS SDK is developed terribly, Spotify developers are so incompetent that they don't even bother to answer. Probably because they don't want to admit it :D
@frangulyan we initialise session using Spotify SDK. After that if we need to refresh access token we use our own implementation by calling our refresh access token endpoints. I also created similar issue https://github.com/spotify/ios-sdk/issues/180. Hope that helps.
@paleksandrs for the last 2 days I was experimenting with the sample app they have. Our app requirements are simple:
After playing with the sample app that spotify provides I came to similar solution like you mentioned - initialize with Spotify SDK and then refresh myself. I noticed the following:
AppRemote has no reference to refresh token (not in APIs at least), which means it is not smart enough to refresh the access token itself. Which means that after 1 hour if you try to pause music then it will fail with "access token not valid" error (interestingly, the debugger shows that appRemote was still "connected" in that case). Although AppRemote is initialized with SPTConfiguration
which has all the swap links and I was hoping that it will be smart enough to refresh the tokens. By the way, this happens randomly, in some cases even after 1 hour the App Remote is still connected normally, play/pause commands are working fine and no swap was called on my server. Have no idea how it works. The official documentation gives no comments on refresh strategies or guaranties that it will handle itself.
Session Manager gives more control over the process, but still not smart enough to call refresh once the expiration date is close. That means that every time I need to do a lot of checks on both app remote and session manager (session is nil, token expiration, app remote connected, error received in any of the delegates, playerAPI is nil, etc) and if any of them fails then I need to refresh the tokens using our backend using sessionManager.renewSession()
.
The documentation doesn't mention anything about how long the codes/tokens can be stored. I assume that, like normally, refresh tokens can be persisted for later use. I assume that authorization code can also be stored, although not sure.
Let me know if you have any comment / experience you can share on the points above, that would be very helpful.
We didn't need to use AppRemote
, but I believe you would need to reset access token on it. sessionManager.renewSession()
didn't work for us, because after calling that method it removed refresh token from sessionManager.session
so the next call to refresh access token would fail. Also SPTSession
doesn't have any init
and all properties are read only, so that means that you can't store your refresh token or reset it on SPTSession
.
@paleksandrs thanks for your help!
I guess I will do the following - initially use sessionManager
to get the authorization code and refresh token using Spotify's login UI, and then persist it in the app myself. From that point on I will do everything "manually" with our backend - sending both refresh token and authorization code to get a new access token, if needed. On backend I will first try with refresh token - refresh flow - if I get an error then I will try with authorization code - swap flow (maybe this step doesn't make sense if refresh token doesn't work, have no idea). If both don't work then I will tell back the mobile app to initiate another login using Spotify's sessionManager
SDK since the user might have revoked the access to my app.
Regarding appRemote
- it seems to handle the permissions itself and successfully keep connection for several hours - it might have built-in auth under the hood. However, sometimes it can fail, that's why there is a possibility to set access token on it. With each operation I will just check if everything is fine (a bunch of checks I mentioned in the comment above), if not - get a new access token from our backend, if that fails too - show Spotify SDK's login UI.
Hope this helps the others, would be amazing if Spotify could give some hints or update docs with more details. Their Web API seems to be really good and well thought, I hope that mobile SDK's will catch up soon, at least on the level of clarifying stuff.
Hey everyone, I just stumbled across this looking for the same solution as well.
When trying to re-initiate a new session (when a user opens the app again) it seems there will have to be a hard app-switch involved to restore the session and fetch new tokens.
I was hoping to have a way to just pass in the refresh_token in the configs to the sessionManager, that way it would use them without having to switch over to Spotify first.
Like you all, after the initial auth process I ended up just checking and refreshing them manually when needed on the backend if expired instead of using the session manager.
@jholmes12 the hard app-switch is only needed if:
If I oversimplify my codes, removing different states and checks, I ended up using the following high-level logic:
MySpotifyManager.getTokens { tokens in
guard let tokens = tokens else {
// complete with failure
return
}
self.appRemote.connectionParameters.accessToken = tokens.accessToken
SPTAppRemote.checkIfSpotifyAppIsActive { isActive in
if isActive {
// this will call SPTAppRemoteDelegate functions, you should complete with success or failure depending on the invoked delegate functions
self.appRemote.connect()
} else {
// this will NEVER call SPTAppRemoteDelegate, you need to wait till AppDelegate's application(_ app, open url) is called after the call below and from there, if no error given, you do the same as in the above 'if' branch - need to call appRemote.connect() and wait for SPTAppRemoteDelegate functions to be called to complete with success or failure
self.appRemote.authorizeAndPlayURI(initialTrackId)
}
}
}
MySpotifyManager.getTokens
is doing the full logic of token fetching, I call it the Session Manager phase - checking if refresh token is cached, using session manager to get auth code if not (will do the app switch in-between), calling backend to get tokens based on auth code or currently saved tokens, handling errors from backend (which might happen if the cached refresh token is invalidated for some reason), and so on. If this function returns nil then it's bad, if not - I'm going to the next phase, the App Remote phase, which is shown in the closure - setting the access token and trying to connect the app remote (starting the Spotify app before that, if needed).
@frangulyan thanks so much for taking the time to let me know how you are handling that, it all is starting to become more clear now for sure! This sounds like a good way to handle it. It took a little trial and error to account for all the situations of spotify being active or not, or the app remote being connected, but overall definitely making more sense. Appreciate it!
At the risk of the Spotify developers breaking this method in later versions, too, I found a way to persist and recreate SPTSession
objects between launches. At first, I noticed that SPTSession
is declared to conform to NSSecureCoding
, and it seemed to properly encode its contents with NSKeyedArchiver
, though decoding it with NSKeyedUnarchiver
always returned nil
.
While -[SPTSession init]
is marked as NS_UNAVAILABLE
, +[SPTSession new]
still functions just fine. Furthermore, while all of SPTSession
's property accessors are readonly
, setting their value via setValue:forKey:
also works. Using these methods, I made this category:
@interface SPTSession (Archiving)
/// @brief Stores the data for the current session in @c +[NSUserDefaults @c standardUserDefaults]
- (void)archive;
/**
@brief Creates a new @c SPTSession using the data that's stored in @c +[NSUserDefaults @c standardUserDefaults]
@return If there is session data stored in @c +[NSUserDefaults @c standardUserDefaults], a new @c SPTSession with configured with that data. If there is no or incomplete data, @c nil
*/
+ (nullable SPTSession *)archivedSession;
/**
@brief Removes any session data stored in @c +[NSUserDefaults @c standardUserDefaults]
@discussion Consider calling this method when the user intends to log out of Spotify
*/
+ (void)removeArchivedSession;
@end
#import "SPTSession+Archiving.h"
/// @brief Key constants for @c NSUserDefaults to correspond to the properties of @c SPTSession
typedef NSString * SPTSessionArchivingKey NS_TYPED_ENUM;
/// @brief Key constant for @c NSUserDefaults to correspond to the session's @c accessToken
static const SPTSessionArchivingKey SPTSessionArchivingKeyAccessToken = @"SPTSessionArchivingKeyAccessToken";
/// @brief Key constant for @c NSUserDefaults to correspond to the session's @c refreshToken
static const SPTSessionArchivingKey SPTSessionArchivingKeyRefreshToken = @"SPTSessionArchivingKeyRefreshToken";
/// @brief Key constant for @c NSUserDefaults to correspond to the session's @c expirationDate
static const SPTSessionArchivingKey SPTSessionArchivingKeyExpirationDate = @"SPTSessionArchivingKeyExpirationDate";
/// @brief Key constant for @c NSUserDefaults to correspond to the session's @c scope
static const SPTSessionArchivingKey SPTSessionArchivingKeyScope = @"SPTSessionArchivingKeyScope";
@implementation SPTSession (Archiving)
- (void)archive {
[NSUserDefaults.standardUserDefaults setObject:self.accessToken forKey:SPTSessionArchivingKeyAccessToken];
[NSUserDefaults.standardUserDefaults setObject:self.refreshToken forKey:SPTSessionArchivingKeyRefreshToken];
[NSUserDefaults.standardUserDefaults setInteger:self.expirationDate.timeIntervalSinceReferenceDate forKey:SPTSessionArchivingKeyExpirationDate];
[NSUserDefaults.standardUserDefaults setInteger:self.scope forKey:SPTSessionArchivingKeyScope];
}
+ (nullable SPTSession *)archivedSession {
NSString * const storedAccessToken = [NSUserDefaults.standardUserDefaults stringForKey:SPTSessionArchivingKeyAccessToken];
NSString * const storedRefreshToken = [NSUserDefaults.standardUserDefaults stringForKey:SPTSessionArchivingKeyRefreshToken];
NSDate * const storedExpirationDate = [NSDate dateWithTimeIntervalSinceReferenceDate:[NSUserDefaults.standardUserDefaults integerForKey:SPTSessionArchivingKeyExpirationDate]];
if (storedAccessToken && storedRefreshToken && storedExpirationDate) {
SPTSession * const storedSession = [SPTSession new];
[storedSession setValue:storedAccessToken forKey:NSStringFromSelector(@selector(accessToken))];
[storedSession setValue:storedRefreshToken forKey:NSStringFromSelector(@selector(refreshToken))];
[storedSession setValue:storedExpirationDate forKey:NSStringFromSelector(@selector(expirationDate))];
[storedSession setValue:@([NSUserDefaults.standardUserDefaults integerForKey:SPTSessionArchivingKeyScope]) forKey:NSStringFromSelector(@selector(scope))];
return storedSession;
} else {
return nil;
}
}
+ (void)removeArchivedSession {
for (SPTSessionArchivingKey key in @[SPTSessionArchivingKeyAccessToken, SPTSessionArchivingKeyRefreshToken, SPTSessionArchivingKeyExpirationDate, SPTSessionArchivingKeyScope]) {
[NSUserDefaults.standardUserDefaults removeObjectForKey:key];
}
}
@end
I'm also struggling with this. To test, I have taken the sample app SPTLoginSampleAppSwift. I have added functionality to save the access token and refresh token using UserDefaults. I then have a renew button which tries to renew the session like so:
sessionManager.session?.setValue(self.accessToken, forKey: "accessToken")
sessionManager.session?.setValue(self.refreshToken, forKey: "refreshToken")
sessionManager.renewSession()
If I load up my app and connect to Spotify. Then press the renew button, the renewal works. But if I close my app, reopen it and press the renew button (thus loading up the session details from UserDefaults. Authorization does not work.
Do I need to call my token swap server URL to swap to token before renewing the session? I assumed the renewSession call was taking care of that part.
The solution to persistent storage of the session is incredibly simple: just encode and decode the SPTSession
.
I've been able to successfully encode SPTSession
using NSKeyedArchiver
and decode it using NSKeyedUnarchiver
.
Make sure you encode it in both sessionManager(manager:didInitiate:)
and sessionManager(manager:didRenew:)
:
let sessionData: Data = try NSKeyedArchiver.archivedData(
withRootObject: session,
requiringSecureCoding: false
)
Then decode it later like this:
let object = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(sessionData)
let session = object as? SPTSession
I still don't understand why the SPTSession
initializer is not publicly exposed. Some clients may wish to create an instance from authorization information that was retrieved from a source other than the iOS SDK.
I got saved sessions working but now I have a new problem... if the user logs out of Spotify and logs into another account from the Spotify app, is there any way to tell that the cached session I have is now logged into the wrong account?
@xabaddonx1 how did you get this to work? Here is my ViewController, a near identical VC to the sample app SPTLoginSampleAppSwift
in this repo. I am restoring the session from local storage as well as updating the appRemote's accessToken
and yet, each time the session is restored I am taken to Spotify and back for the SDK to retrieve an access token. Any thoughts on what I am doing wrong here?
The solution to persistent storage of the session is incredibly simple: just encode and decode the
SPTSession
.I've been able to successfully encode
SPTSession
usingNSKeyedArchiver
and decode it usingNSKeyedUnarchiver
.Make sure you encode it in both
sessionManager(manager:didInitiate:)
andsessionManager(manager:didRenew:)
:let sessionData: Data = try NSKeyedArchiver.archivedData( withRootObject: session, requiringSecureCoding: false )
Then decode it later like this:
let object = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(sessionData) let session = object as? SPTSession
I still don't understand why the
SPTSession
initializer is not publicly exposed. Some clients may wish to create an instance from authorization information that was retrieved from a source other than the iOS SDK.
It's unbelievable that this is not on the official documentation. The Spotify SDK is probably the weirdest SDK I ever had to deal with, I can't comprehend how badly designed it is. Thanks for sharing this, spent half a day trying to find out a solution to this.
Is there a good way to continue an active session from stored values? I'm storing the raw values of
accessToken
,refreshToken
,expirationDate
, andscope
in a persistent storage, and I would love to use those to create a new SPTSession object that an SPTSessionManager can then manage.E.g.:
Or better yet:
The goal here would be to continue an existing user's session without forcing them to go through the authentication flow again even if we've lost the SPTSessionManager that originated the session. Any help / suggestions here would be appreciated!