diff --git a/sample/Hackbook/Hackbook/APIResultsViewController.h b/sample/Hackbook/Hackbook/APIResultsViewController.h index 056839a611..055ba34986 100644 --- a/sample/Hackbook/Hackbook/APIResultsViewController.h +++ b/sample/Hackbook/Hackbook/APIResultsViewController.h @@ -19,7 +19,6 @@ @interface APIResultsViewController : UIViewController { NSMutableArray *myData; diff --git a/sample/Hackbook/Hackbook/HackbookAppDelegate.m b/sample/Hackbook/Hackbook/HackbookAppDelegate.m index 76a1ae61cc..d93debee27 100644 --- a/sample/Hackbook/Hackbook/HackbookAppDelegate.m +++ b/sample/Hackbook/Hackbook/HackbookAppDelegate.m @@ -43,37 +43,44 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; [navController.navigationBar setTintColor:[UIColor colorWithRed:0/255.0 green:51.0/255.0 - blue:102.0/255.0 + blue:102.0/255.0 alpha:1.0]]; [navController.navigationBar setBarStyle:UIBarStyleBlackTranslucent]; self.navigationController = navController; [rootViewController release]; [navController release]; - + // Initialize Facebook facebook = [[Facebook alloc] initWithAppId:kAppId andDelegate:rootViewController]; - + + // Check and retrieve authorization information + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + if ([defaults objectForKey:@"FBAccessTokenKey"] && [defaults objectForKey:@"FBExpirationDateKey"]) { + facebook.accessToken = [defaults objectForKey:@"FBAccessTokenKey"]; + facebook.expirationDate = [defaults objectForKey:@"FBExpirationDateKey"]; + } + // Initialize API data (for views, etc.) apiData = [[DataSet alloc] init]; - + // Initialize user permissions userPermissions = [[NSMutableDictionary alloc] initWithCapacity:1]; - + // Override point for customization after application launch. // Add the navigation controller's view to the window and display. self.window.rootViewController = self.navigationController; [self.window makeKeyAndVisible]; - + // Check App ID: // This is really a warning for the developer, this should not // happen in a completed app if (!kAppId) { - UIAlertView *alertView = [[UIAlertView alloc] - initWithTitle:@"Setup Error" - message:@"Missing app ID. You cannot run the app until you provide this in the code." - delegate:self - cancelButtonTitle:@"OK" - otherButtonTitles:nil, + UIAlertView *alertView = [[UIAlertView alloc] + initWithTitle:@"Setup Error" + message:@"Missing app ID. You cannot run the app until you provide this in the code." + delegate:self + cancelButtonTitle:@"OK" + otherButtonTitles:nil, nil]; [alertView show]; [alertView release]; @@ -83,7 +90,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( NSString *url = [NSString stringWithFormat:@"fb%@://authorize",kAppId]; BOOL bSchemeInPlist = NO; // find out if the sceme is in the plist file. NSArray* aBundleURLTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"]; - if ([aBundleURLTypes isKindOfClass:[NSArray class]] && + if ([aBundleURLTypes isKindOfClass:[NSArray class]] && ([aBundleURLTypes count] > 0)) { NSDictionary* aBundleURLTypes0 = [aBundleURLTypes objectAtIndex:0]; if ([aBundleURLTypes0 isKindOfClass:[NSDictionary class]]) { @@ -91,7 +98,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( if ([aBundleURLSchemes isKindOfClass:[NSArray class]] && ([aBundleURLSchemes count] > 0)) { NSString *scheme = [aBundleURLSchemes objectAtIndex:0]; - if ([scheme isKindOfClass:[NSString class]] && + if ([scheme isKindOfClass:[NSString class]] && [url hasPrefix:scheme]) { bSchemeInPlist = YES; } @@ -101,21 +108,29 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // Check if the authorization callback will work BOOL bCanOpenUrl = [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString: url]]; if (!bSchemeInPlist || !bCanOpenUrl) { - UIAlertView *alertView = [[UIAlertView alloc] - initWithTitle:@"Setup Error" - message:@"Invalid or missing URL scheme. You cannot run the app until you set up a valid URL scheme in your .plist." - delegate:self - cancelButtonTitle:@"OK" - otherButtonTitles:nil, + UIAlertView *alertView = [[UIAlertView alloc] + initWithTitle:@"Setup Error" + message:@"Invalid or missing URL scheme. You cannot run the app until you set up a valid URL scheme in your .plist." + delegate:self + cancelButtonTitle:@"OK" + otherButtonTitles:nil, nil]; [alertView show]; [alertView release]; } } - + return YES; } +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Although the SDK attempts to refresh its access tokens when it makes API calls, + // it's a good practice to refresh the access token also when the app becomes active. + // This gives apps that seldom make api calls a higher chance of having a non expired + // access token. + [[self facebook] extendAccessTokenIfNeeded]; +} + - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url { return [self.facebook handleOpenURL:url]; } diff --git a/sample/Hackbook/Hackbook/RootViewController.m b/sample/Hackbook/Hackbook/RootViewController.m index bc80c4557b..d0e3dc26ce 100644 --- a/sample/Hackbook/Hackbook/RootViewController.m +++ b/sample/Hackbook/Hackbook/RootViewController.m @@ -103,7 +103,7 @@ - (void)showLoggedOut { nameLabel.text = @""; // Get the profile image [profilePhotoImageView setImage:nil]; - + [[self navigationController] popToRootViewControllerAnimated:YES]; } @@ -245,19 +245,11 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; HackbookAppDelegate *delegate = (HackbookAppDelegate *)[[UIApplication sharedApplication] delegate]; - // Check and retrieve authorization information - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - if ([defaults objectForKey:@"FBAccessTokenKey"] - && [defaults objectForKey:@"FBExpirationDateKey"]) { - [delegate facebook].accessToken = [defaults objectForKey:@"FBAccessTokenKey"]; - [delegate facebook].expirationDate = [defaults objectForKey:@"FBExpirationDateKey"]; - } if (![[delegate facebook] isSessionValid]) { [self showLoggedOut]; } else { [self showLoggedIn]; } - } - (void)viewWillDisappear:(BOOL)animated { @@ -315,6 +307,13 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } +- (void)storeAuthData:(NSString *)accessToken expiresAt:(NSDate *)expiresAt { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setObject:accessToken forKey:@"FBAccessTokenKey"]; + [defaults setObject:expiresAt forKey:@"FBExpirationDateKey"]; + [defaults synchronize]; +} + #pragma mark - FBSessionDelegate Methods /** * Called when the user has logged in successfully. @@ -323,16 +322,16 @@ - (void)fbDidLogin { [self showLoggedIn]; HackbookAppDelegate *delegate = (HackbookAppDelegate *)[[UIApplication sharedApplication] delegate]; + [self storeAuthData:[[delegate facebook] accessToken] expiresAt:[[delegate facebook] expirationDate]]; - // Save authorization information - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - [defaults setObject:[[delegate facebook] accessToken] forKey:@"FBAccessTokenKey"]; - [defaults setObject:[[delegate facebook] expirationDate] forKey:@"FBExpirationDateKey"]; - [defaults synchronize]; - [pendingApiCallsController userDidGrantPermission]; } +-(void)fbDidExtendToken:(NSString *)accessToken expiresAt:(NSDate *)expiresAt { + NSLog(@"token extended"); + [self storeAuthData:accessToken expiresAt:expiresAt]; +} + /** * Called when the user canceled the authorization dialog. */ @@ -359,13 +358,13 @@ - (void)fbDidLogout { /** * Called when the session has expired. */ -- (void)fbSessionInvalidated { +- (void)fbSessionInvalidated { UIAlertView *alertView = [[UIAlertView alloc] - initWithTitle:@"Auth Exception" - message:@"Your session has expired." - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil, + initWithTitle:@"Auth Exception" + message:@"Your session has expired." + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil, nil]; [alertView show]; [alertView release]; diff --git a/src/Facebook.h b/src/Facebook.h index f4cea27c89..8620fc9739 100644 --- a/src/Facebook.h +++ b/src/Facebook.h @@ -25,7 +25,7 @@ * and Graph APIs, and start user interface interactions (such as * pop-ups promoting for credentials, permissions, stream posts, etc.) */ -@interface Facebook : NSObject{ +@interface Facebook : NSObject{ NSString* _accessToken; NSDate* _expirationDate; id _sessionDelegate; @@ -35,6 +35,8 @@ NSString* _appId; NSString* _urlSchemeSuffix; NSArray* _permissions; + BOOL _isExtendingAccessToken; + NSDate* _lastAccessTokenUpdate; } @property(nonatomic, copy) NSString* accessToken; @@ -51,6 +53,12 @@ - (void)authorize:(NSArray *)permissions; +- (void)extendAccessToken; + +- (void)extendAccessTokenIfNeeded; + +- (BOOL)shouldExtendAccessToken; + - (BOOL)handleOpenURL:(NSURL *)url; - (void)logout; @@ -93,8 +101,6 @@ */ @protocol FBSessionDelegate -@optional - /** * Called when the user successfully logged in. */ @@ -105,6 +111,16 @@ */ - (void)fbDidNotLogin:(BOOL)cancelled; +/** + * Called after the access token was extended. If your application has any + * references to the previous access token (for example, if your application + * stores the previous access token in persistent storage), your application + * should overwrite the old access token with the new one in this method. + * See extendAccessToken for more details. + */ +- (void)fbDidExtendToken:(NSString*)accessToken + expiresAt:(NSDate*)expiresAt; + /** * Called when the user logged out. */ @@ -112,7 +128,7 @@ /** * Called when the current session has expired. This might happen when: - * - the access token expired + * - the access token expired * - the app has been disabled * - the user revoked the app's permissions * - the user changed his or her password diff --git a/src/Facebook.m b/src/Facebook.m index 690234df80..4380e84afa 100644 --- a/src/Facebook.m +++ b/src/Facebook.m @@ -30,6 +30,10 @@ static NSString* kSDK = @"ios"; static NSString* kSDKVersion = @"2"; +// If the last time we extended the access token was more than 24 hours ago +// we try to refresh the access token again. +static const int kTokenExtendThreshold = 24; + static NSString *requestFinishedKeyPath = @"state"; static void *finishedContext = @"finishedContext"; @@ -93,10 +97,11 @@ - (id)initWithAppId:(NSString *)appId - (id)initWithAppId:(NSString *)appId urlSchemeSuffix:(NSString *)urlSchemeSuffix andDelegate:(id)delegate { - + self = [super init]; if (self) { _requests = [[NSMutableSet alloc] init]; + _lastAccessTokenUpdate = [[NSDate distantPast] retain]; self.appId = appId; self.sessionDelegate = delegate; self.urlSchemeSuffix = urlSchemeSuffix; @@ -111,6 +116,7 @@ - (void)dealloc { for (FBRequest* _request in _requests) { [_request removeObserver:self forKeyPath:requestFinishedKeyPath]; } + [_lastAccessTokenUpdate release]; [_accessToken release]; [_expirationDate release]; [_requests release]; @@ -125,11 +131,11 @@ - (void)dealloc { - (void)invalidateSession { self.accessToken = nil; self.expirationDate = nil; - + NSHTTPCookieStorage* cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage]; NSArray* facebookCookies = [cookies cookiesForURL: [NSURL URLWithString:@"http://login.facebook.com"]]; - + for (NSHTTPCookie* cookie in facebookCookies) { [cookies deleteCookie:cookie]; } @@ -160,6 +166,8 @@ - (FBRequest*)openUrl:(NSString *)url [params setValue:self.accessToken forKey:@"access_token"]; } + [self extendAccessTokenIfNeeded]; + FBRequest* _request = [FBRequest getRequestWithParams:params httpMethod:httpMethod delegate:delegate @@ -221,7 +229,7 @@ - (void)authorizeWithFBAppAuth:(BOOL)tryFBAppAuth if (_urlSchemeSuffix) { [params setValue:_urlSchemeSuffix forKey:@"local_client_id"]; } - + // If the device is running a version of iOS that supports multitasking, // try to obtain the access token from the Facebook app installed // on the device. @@ -318,6 +326,56 @@ - (void)authorize:(NSArray *)permissions { [self authorizeWithFBAppAuth:YES safariAuth:YES]; } +/** + * Attempt to extend the access token. + * + * Access tokens typically expire within 30-60 days. When the user uses the + * app, the app should periodically try to obtain a new access token. Once an + * access token has expired, the app can no longer renew it. The app then has + * to ask the user to re-authorize it to obtain a new access token. + * + * To ensure your app always has a fresh access token for active users, it's + * recommended that you call extendAccessTokenIfNeeded in your application's + * applicationDidBecomeActive: UIApplicationDelegate method. + */ +- (void)extendAccessToken { + if (_isExtendingAccessToken) { + return; + } + _isExtendingAccessToken = YES; + NSMutableDictionary* params = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"auth.extendSSOAccessToken", @"method", + nil]; + [self requestWithParams:params andDelegate:self]; +} + +/** + * Calls extendAccessToken if shouldExtendAccessToken returns YES. + */ +- (void)extendAccessTokenIfNeeded { + if ([self shouldExtendAccessToken]) { + [self extendAccessToken]; + } +} + +/** + * Returns YES if the last time a new token was obtained was over 24 hours ago. + */ +- (BOOL)shouldExtendAccessToken { + if ([self isSessionValid]){ + NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease]; + NSDateComponents *components = [calendar components:NSHourCalendarUnit + fromDate:_lastAccessTokenUpdate + toDate:[NSDate date] + options:0]; + + if (components.hour >= kTokenExtendThreshold) { + return YES; + } + } + return NO; +} + /** * This function processes the URL the Facebook application or Safari used to * open your application during a single sign-on flow. @@ -408,7 +466,7 @@ - (BOOL)handleOpenURL:(NSURL *)url { */ - (void)logout { [self invalidateSession]; - + if ([self.sessionDelegate respondsToSelector:@selector(fbDidLogout)]) { [self.sessionDelegate fbDidLogout]; } @@ -623,6 +681,7 @@ - (void)dialog:(NSString *)action if ([self isSessionValid]) { [params setValue:[self.accessToken stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] forKey:@"access_token"]; + [self extendAccessTokenIfNeeded]; } _fbDialog = [[FBDialog alloc] initWithURL:dialogURL params:params delegate:delegate]; } @@ -648,6 +707,8 @@ - (BOOL)isSessionValid { - (void)fbDialogLogin:(NSString *)token expirationDate:(NSDate *)expirationDate { self.accessToken = token; self.expirationDate = expirationDate; + [_lastAccessTokenUpdate release]; + _lastAccessTokenUpdate = [[NSDate date] retain]; if ([self.sessionDelegate respondsToSelector:@selector(fbDidLogin)]) { [self.sessionDelegate fbDidLogin]; } @@ -663,4 +724,45 @@ - (void)fbDialogNotLogin:(BOOL)cancelled { } } +#pragma mark - FBRequestDelegate Methods +// These delegate methods are only called for requests that extendAccessToken initiated + +- (void)request:(FBRequest *)request didFailWithError:(NSError *)error { + _isExtendingAccessToken = NO; +} + +- (void)request:(FBRequest *)request didLoad:(id)result { + _isExtendingAccessToken = NO; + NSString* accessToken = [result objectForKey:@"access_token"]; + NSString* expTime = [result objectForKey:@"expires_at"]; + + if (accessToken == nil || expTime == nil) { + return; + } + + self.accessToken = accessToken; + + NSTimeInterval timeInterval = [expTime doubleValue]; + NSDate *expirationDate = [NSDate distantFuture]; + if (timeInterval != 0) { + expirationDate = [NSDate dateWithTimeIntervalSince1970:timeInterval]; + } + self.expirationDate = expirationDate; + [_lastAccessTokenUpdate release]; + _lastAccessTokenUpdate = [[NSDate date] retain]; + + if ([self.sessionDelegate respondsToSelector:@selector(fbDidExtendToken:expiresAt:)]) { + [self.sessionDelegate fbDidExtendToken:accessToken expiresAt:expirationDate]; + } +} + +- (void)request:(FBRequest *)request didLoadRawResponse:(NSData *)data { +} + +- (void)request:(FBRequest *)request didReceiveResponse:(NSURLResponse *)response{ +} + +- (void)requestLoading:(FBRequest *)request{ +} + @end