Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PROD-835] Push notifications #221

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4cd57e4
PROD-835 first stab at a rough copy of push notifications in pusher-w…
jameshfisher Jul 26, 2016
cceae46
expose the rest of the API
jameshfisher Jul 27, 2016
243ece6
add to example ios
jameshfisher Jul 27, 2016
09e0535
commit magic file
jameshfisher Jul 27, 2016
9d5d679
copy const
jameshfisher Jul 27, 2016
5da1c9f
handle json serialization error
jameshfisher Jul 27, 2016
6576165
remove method - satisfied by property
jameshfisher Jul 27, 2016
aa6012a
log when received device token in sample app
jameshfisher Jul 27, 2016
74dc72e
add development section to README
jameshfisher Jul 27, 2016
e98aeb6
add magic file again
jameshfisher Jul 27, 2016
9f9a0f7
fix disgraceful logic
jameshfisher Jul 27, 2016
b5d7c53
house rules
jameshfisher Jul 27, 2016
727a0fc
add some error logging
jameshfisher Jul 27, 2016
44ef422
json is encoded in utf-8
jameshfisher Jul 27, 2016
0d37475
add magic file
jameshfisher Jul 27, 2016
a9db9c5
empty stub unit test file
jameshfisher Jul 28, 2016
d6580e1
a dummy test
jameshfisher Jul 28, 2016
15c8814
add callback for when registered
jameshfisher Jul 28, 2016
07dfc9b
add something resembling a real test
jameshfisher Jul 28, 2016
1aff281
add delegate callbacks for subscribe/unsubscribe
jameshfisher Jul 28, 2016
4810feb
test subscribe/unsubscribe
jameshfisher Jul 28, 2016
07d54a9
i think this was the problem
jameshfisher Jul 28, 2016
99ba736
reset the pbxproj file
jameshfisher Jul 29, 2016
3befe05
describe push notifications api in the readme
jameshfisher Jul 29, 2016
1db090a
add logging callbacks to delegate
jameshfisher Jul 29, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions Library/PTNativePusher.h
@@ -0,0 +1,19 @@
//
// PTNativePusher.h
// libPusher
//
// Created by James Fisher on 25/07/2016.
//
//

#ifndef PTNativePusher_h
#define PTNativePusher_h

@interface PTNativePusher : NSObject

// Takes ownership of the pusherAppKey string
- (id)initWithPusherAppKey:(NSString *)pusherAppKey;

@end

#endif /* PTNativePusher_h */
159 changes: 159 additions & 0 deletions Library/PTNativePusher.m
@@ -0,0 +1,159 @@
//
// PTNativePusher.m
// libPusher
//
// Created by James Fisher on 25/07/2016.
//
//

#import "PTNativePusher.h"

NSString *const CLIENT_API_V1_ENDPOINT = @"https://nativepushclient-cluster1.pusher.com/client_api/v1";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rewrite as

#define kPUSHER_CLIENT_API_V1_ENDPOINT @"https://nativepushclient-cluster1.pusher.com/client_api/v1";


const int MAX_FAILED_REQUEST_ATTEMPTS = 6;

// FIXME free() of below vars? ref counting?
@implementation PTNativePusher {
NSURLSession * urlSession;
int failedNativeServiceRequests;
NSString * pusherAppKey;
NSString * clientId;
NSMutableArray * outbox;
}

- (id)initWithPusherAppKey:(NSString *)_pusherAppKey {
if (self = [super init]) {
self->urlSession = [NSURLSession sharedSession];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with the self-> syntax and I'm not really sure what it's doing.

Typically I've seen init methods written more like this: https://github.com/pusher/libPusher/blob/master/Library/PTPusherAPI.m#L23-L32

where you just do something like urlSession = [NSURLSession sharedSession]

self->failedNativeServiceRequests = 0;
self->pusherAppKey = _pusherAppKey;
self->clientId = NULL; // NULL until we register
self->outbox = [NSMutableArray array];
}
return self;
}

// Lovingly borrowed from http://stackoverflow.com/a/16411517/229792
- (NSString *) deviceTokenToString:(NSData *)deviceToken {
const char *data = [deviceToken bytes];
NSMutableString *token = [NSMutableString string];

for (NSUInteger i = 0; i < [deviceToken length]; i++) {
[token appendFormat:@"%02.2hhX", data[i]];
}

return [token copy];
}

- (void) registerWithDeviceToken: (NSData*) deviceToken {
NSMutableURLRequest * request =
[NSMutableURLRequest requestWithURL:[NSURL URLWithString: [NSString stringWithFormat:@"%@%@", CLIENT_API_V1_ENDPOINT, @"/clients"]]];
[request setHTTPMethod:@"POST"];

NSString* deviceTokenString = [self deviceTokenToString:deviceToken];

NSDictionary * params = @{
@"app_key": self->pusherAppKey,
@"platform_type": @"apns", // FIXME constant
@"token": deviceTokenString
// TODO client name/version
};

assert([NSJSONSerialization isValidJSONObject:params]);

[request setHTTPBody:[NSJSONSerialization dataWithJSONObject:params options:@[] error: NULL]]; // FIXME error NULL?

// FIXME what encoding does the above ^ serialization use? UTF-8?
[request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];

NSURLSessionDataTask * task = [urlSession dataTaskWithRequest:request
completionHandler: ^(NSData * data, NSURLResponse * response, NSError * error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*) response;
if ([httpResponse statusCode] >= 200 && [httpResponse statusCode] < 300) {
NSError * jsonDecodingError;
id jsonObj = [NSJSONSerialization JSONObjectWithData:data options:@[] error:&jsonDecodingError];
// FIXME check jsonDecodingError
NSDictionary* jsonDict = (NSDictionary*) jsonObj;
NSObject * clientIdObj = [jsonDict objectForKey:@"id"];
NSString * clientIdString = (NSString*) clientIdObj;
self->clientId = clientIdString;
[self tryFlushOutbox];
} else {
// TODO error
}
}];
[task resume];
}

- (void) subscribe:(NSString *)interestName {
// TODO using a dictionary here is kinda horrible
[self->outbox addObject:@{ @"interestName": interestName, @"change": @"subscribe" }];
[self tryFlushOutbox];
}

- (void) unsubscribe:(NSString *)interestName {
[self->outbox addObject:@{ @"interestName": interestName, @"change": @"unsubscribe" }];
[self tryFlushOutbox];
}

- (void) tryFlushOutbox {
if (self->clientId != NULL && 0 < [self->outbox count]) {
NSDictionary* subscriptionChange = (NSDictionary*) [self->outbox objectAtIndex:0];
NSString* interestName = (NSString*) [subscriptionChange objectForKey:@"interestName"];
NSString* change = (NSString*) [subscriptionChange objectForKey:@"change"];
[self
modifySubscriptionForPusherAppKey:self->pusherAppKey
clientId:self->clientId
interestName:interestName
subscriptionChange:change
callback: ^() { [self tryFlushOutbox]; }];
}
}

- (void) modifySubscriptionForPusherAppKey:(NSString*) _pusherAppKey clientId: (NSString*) _clientId interestName: (NSString*) interestName subscriptionChange: (NSString*) subscriptionChange callback: (void(^)(void)) callback {
NSString* url = [NSString stringWithFormat:@"%@/clients/%@/interests/%@", CLIENT_API_V1_ENDPOINT, _clientId, interestName];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL: [NSURL URLWithString:url]];

if ([subscriptionChange isEqualToString:@"subscribe"]) {
[request setHTTPMethod:@"POST"];
} else if ([subscriptionChange isEqualToString:@"subscribe"]) {
[request setHTTPMethod:@"DELETE"];
}

NSDictionary* params = @{
@"app_key": _pusherAppKey
// TODO client name/version
};

[request setHTTPBody:[NSJSONSerialization dataWithJSONObject:params options:@[] error:NULL]]; // TODO error??
[request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; // TODO charset

NSURLSessionDataTask* task = [urlSession dataTaskWithRequest:request completionHandler:^(NSData * data, NSURLResponse * response, NSError * error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*) response;
if ([httpResponse statusCode] >= 200 && [httpResponse statusCode] < 300) {
// Reset number of failed requests to 0 upon success
self->failedNativeServiceRequests = 0;

callback();
} else {
[self->outbox insertObject:@{ @"interestName": interestName, @"change": subscriptionChange } atIndex:0];

if (error != nil) {
// TODO print error
} else {
// TODO print error
}

self->failedNativeServiceRequests += 1;

if (self->failedNativeServiceRequests < MAX_FAILED_REQUEST_ATTEMPTS) {
callback();
} else {
// TODO print error
}
// TODO error
}
}];
[task resume];
}

@end
15 changes: 15 additions & 0 deletions Library/PTPusher.h
Expand Up @@ -12,6 +12,7 @@
#import "PTPusherEventPublisher.h"
#import "PTPusherPresenceChannelDelegate.h"
#import "PTPusherChannelAuthorizationDelegate.h"
#import "PTNativePusher.h"

/** The name of the notification posted when PTPusher receives an event.
*
Expand Down Expand Up @@ -138,6 +139,13 @@ extern NSString *const PTPusherErrorUnderlyingEventKey;
*/
@property (nonatomic, weak) id<PTPusherChannelAuthorizationDelegate> channelAuthorizationDelegate;

/** Provides the native push notification API.
*
* PTNativePusher is expected to be used as a singleton accessed via this property.
*/
@property (nonatomic, strong) PTNativePusher* nativePusher;


///------------------------------------------------------------------------------------/
/// @name Creating new instances
///------------------------------------------------------------------------------------/
Expand Down Expand Up @@ -315,5 +323,12 @@ extern NSString *const PTPusherErrorUnderlyingEventKey;
*/
- (void)sendEventNamed:(NSString *)name data:(id)data channel:(NSString *)channelName;


///------------------------------------------------------------------------------------/
/// Push Notifications
///------------------------------------------------------------------------------------/

- (PTNativePusher *)nativePusher;

@end

12 changes: 11 additions & 1 deletion Library/PTPusher.m
Expand Up @@ -76,7 +76,7 @@ - (id)initWithConnection:(PTPusherConnection *)connection
self.connection = connection;
self.connection.delegate = self;
self.reconnectDelay = kPTPusherDefaultReconnectDelay;

/* Three reconnection attempts should be more than enough attempts
* to reconnect where the user has simply locked their device or
* backgrounded the app.
Expand Down Expand Up @@ -115,6 +115,9 @@ + (instancetype)pusherWithKey:(NSString *)key delegate:(id<PTPusherDelegate>)del
PTPusherConnection *connection = [[PTPusherConnection alloc] initWithURL:serviceURL];
PTPusher *pusher = [[self alloc] initWithConnection:connection];
pusher.delegate = delegate;

pusher.nativePusher = [[PTNativePusher alloc] initWithPusherAppKey:key];

return pusher;
}

Expand Down Expand Up @@ -460,6 +463,13 @@ - (void)handleDisconnection:(PTPusherConnection *)connection error:(NSError *)er
}
}

#pragma mark - Push Notifications

- (PTNativePusher*) nativePusher
{
return self.nativePusher;
}

#pragma mark - Private

- (void)reconnectUsingMode:(PTPusherAutoReconnectMode)reconnectMode
Expand Down