├── Example ├── Sample Project │ ├── en.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ ├── fr.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ ├── Images.xcassets │ │ ├── Appbot.imageset │ │ │ ├── appbotx-logo.png │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── LaunchImage.launchimage │ │ │ └── Contents.json │ ├── it.lproj │ │ └── Localizable.strings │ ├── ABXViewController.h │ ├── ABXAppDelegate.h │ ├── main.m │ ├── Sample Project-Prefix.pch │ ├── Sample Project-Info.plist │ ├── ABXAppDelegate.m │ ├── Launch Screen.xib │ ├── ABXViewController.m │ └── Storyboard.storyboard └── Sample Project.xcodeproj │ └── project.xcworkspace │ └── contents.xcworkspacedata ├── Classes ├── AppbotX.bundle │ ├── en.lproj │ │ └── Localizable.strings │ ├── es.lproj │ │ └── Localizable.strings │ ├── fr.lproj │ │ └── Localizable.strings │ ├── ja.lproj │ │ └── Localizable.strings │ ├── zh.lproj │ │ └── Localizable.strings │ └── zh-hant.lproj │ │ └── Localizable.strings ├── Controllers │ ├── ABXNavigationController.h │ ├── ABXNotificationsViewController.h │ ├── ABXVersionsViewController.h │ ├── ABXFAQViewController.h │ ├── ABXBaseListViewController.h │ ├── ABXNavigationController.m │ ├── ABXFAQsViewController.h │ ├── ABXFeedbackViewController.h │ ├── ABXVersionsViewController.m │ ├── ABXNotificationsViewController.m │ ├── ABXBaseListViewController.m │ ├── ABXFAQsViewController.m │ └── ABXFAQViewController.m ├── Views │ ├── ABXTextView.h │ ├── ABXFAQTableViewCell.h │ ├── ABXVersionTableViewCell.h │ ├── ABXNotificationTableViewCell.h │ ├── ABXVersionNotificationView.h │ ├── ABXPromptView.h │ ├── ABXNotificationView.h │ ├── ABXFAQTableViewCell.m │ ├── ABXTextView.m │ ├── ABXVersionTableViewCell.m │ ├── ABXNotificationTableViewCell.m │ ├── ABXVersionNotificationView.m │ ├── ABXPromptView.m │ └── ABXNotificationView.m ├── Classes │ ├── NSDictionary+ABXQueryString.h │ ├── NSString+ABXLocalized.h │ ├── NSDictionary+ABXNSNullAsNull.h │ ├── UIViewController+ABXScreenshot.h │ ├── NSString+ABXURLEncoding.h │ ├── ABXAppStore.h │ ├── NSString+ABXSizing.h │ ├── NSDictionary+ABXNSNullAsNull.m │ ├── NSDictionary+ABXQueryString.m │ ├── UIViewController+ABXScreenshot.m │ ├── NSString+ABXURLEncoding.m │ ├── ABXAppStore.m │ ├── ABXApiClient.h │ ├── NSString+ABXSizing.m │ ├── ABXKeychain.h │ ├── NSString+ABXLocalized.m │ ├── ABXKeychain.m │ └── ABXApiClient.m ├── ABX.h └── Models │ ├── ABXAttachment.h │ ├── ABXIssue.h │ ├── ABXModel.h │ ├── ABXNotification.h │ ├── ABXFaq.h │ ├── ABXVersion.h │ ├── ABXAttachment.m │ ├── ABXModel.m │ ├── ABXNotification.m │ ├── ABXFaq.m │ ├── ABXIssue.m │ └── ABXVersion.m ├── .gitignore ├── LICENSE ├── WordPress-AppbotX.podspec ├── Rakefile └── README.md /Example/Sample Project/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Example/Sample Project/fr.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Classes/AppbotX.bundle/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/appbotx/master/Classes/AppbotX.bundle/en.lproj/Localizable.strings -------------------------------------------------------------------------------- /Classes/AppbotX.bundle/es.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/appbotx/master/Classes/AppbotX.bundle/es.lproj/Localizable.strings -------------------------------------------------------------------------------- /Classes/AppbotX.bundle/fr.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/appbotx/master/Classes/AppbotX.bundle/fr.lproj/Localizable.strings -------------------------------------------------------------------------------- /Classes/AppbotX.bundle/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/appbotx/master/Classes/AppbotX.bundle/ja.lproj/Localizable.strings -------------------------------------------------------------------------------- /Classes/AppbotX.bundle/zh.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/appbotx/master/Classes/AppbotX.bundle/zh.lproj/Localizable.strings -------------------------------------------------------------------------------- /Classes/AppbotX.bundle/zh-hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/appbotx/master/Classes/AppbotX.bundle/zh-hant.lproj/Localizable.strings -------------------------------------------------------------------------------- /Example/Sample Project/Images.xcassets/Appbot.imageset/appbotx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/appbotx/master/Example/Sample Project/Images.xcassets/Appbot.imageset/appbotx-logo.png -------------------------------------------------------------------------------- /Example/Sample Project.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Sample Project/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Sample Project 4 | 5 | Created by Stuart Hall on 3/07/2014. 6 | Copyright (c) 2014 Appbot. All rights reserved. 7 | */ 8 | 9 | "How can we help?" = "How can we help you with Sample App?"; -------------------------------------------------------------------------------- /Example/Sample Project/fr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Sample Project 4 | 5 | Created by Stuart Hall on 3/07/2014. 6 | Copyright (c) 2014 Appbot. All rights reserved. 7 | */ 8 | 9 | "How can we help?" = "Comment puis-je vous aider Sample App?"; -------------------------------------------------------------------------------- /Example/Sample Project/it.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Sample Project 4 | 5 | Created by Stuart Hall on 3/07/2014. 6 | Copyright (c) 2014 Appbot. All rights reserved. 7 | */ 8 | 9 | "How can we help?" = "How can we help you with Sample App?"; -------------------------------------------------------------------------------- /Classes/Controllers/ABXNavigationController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXNavigationController.h 3 | // 4 | // Created by Stuart Hall on 11/06/2014. 5 | // Copyright (c) 2014 Stuart Hall. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface ABXNavigationController : UINavigationController 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /Example/Sample Project/ABXViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXViewController.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 21/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ABXViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Classes/Views/ABXTextView.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXTextView.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 30/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ABXTextView : UITextView 12 | 13 | @property (nonatomic, strong) NSString *placeholder; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Classes/Classes/NSDictionary+ABXQueryString.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+ABXQueryString.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface NSDictionary (ABXQueryString) 11 | 12 | - (NSString *)queryStringValue; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Classes/Classes/NSString+ABXLocalized.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+ABXLocalized.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 26/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSString (ABXLocalized) 12 | 13 | - (NSString*)localizedString; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Classes/Classes/NSDictionary+ABXNSNullAsNull.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+ABXNSNullAsNull.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface NSDictionary (ABXNSNullAsNull) 11 | 12 | - (id)objectForKeyNulled:(id)aKey; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Classes/Classes/UIViewController+ABXScreenshot.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+ABXScreenshot.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 30/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIViewController (ABXScreenshot) 12 | 13 | - (UIImage*)takeScreenshot; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXNotificationsViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXNotificationsViewController.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 18/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXBaseListViewController.h" 10 | 11 | @interface ABXNotificationsViewController : ABXBaseListViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXVersionsViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXVersionsViewController.h 3 | // 4 | // Created by Stuart Hall on 22/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "ABXBaseListViewController.h" 11 | 12 | @interface ABXVersionsViewController : ABXBaseListViewController 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Classes/Classes/NSString+ABXURLEncoding.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+ABXURLEncoding.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface NSString (ABXURLEncoding) 11 | 12 | - (NSString *)urlEncodedString; 13 | - (NSString *)urlDecodedString; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Example/Sample Project/Images.xcassets/Appbot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "appbotx-logo.png" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /Example/Sample Project/ABXAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXAppDelegate.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 21/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ABXAppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Classes/Classes/ABXAppStore.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXAppStore.h 3 | // 4 | // Created by Stuart Hall on 18/06/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface ABXAppStore : NSObject 11 | 12 | + (void)openAppStoreReviewForApp:(NSString*)itunesId; 13 | + (void)openAppStoreForApp:(NSString*)itunesId; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Classes/Classes/NSString+ABXSizing.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+ABXSizing.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 12/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSString (ABXSizing) 12 | 13 | - (CGFloat)heightForWidth:(CGFloat)width andFont:(UIFont*)font; 14 | - (CGFloat)widthToFitFont:(UIFont*)font; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Classes/ABX.h: -------------------------------------------------------------------------------- 1 | #import "ABXApiClient.h" 2 | 3 | #import "ABXFaq.h" 4 | #import "ABXNotification.h" 5 | #import "ABXVersion.h" 6 | #import "ABXIssue.h" 7 | 8 | #import "ABXFAQsViewController.h" 9 | #import "ABXVersionsViewController.h" 10 | #import "ABXFeedbackViewController.h" 11 | #import "ABXNotificationsViewController.h" 12 | 13 | #import "ABXNotificationView.h" 14 | #import "ABXVersionNotificationView.h" 15 | 16 | #import "ABXAppStore.h" -------------------------------------------------------------------------------- /Example/Sample Project/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 21/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "ABXAppDelegate.h" 12 | 13 | int main(int argc, char * argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([ABXAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Classes/Views/ABXFAQTableViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXFAQTableViewCell.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 15/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class ABXFaq; 12 | 13 | @interface ABXFAQTableViewCell : UITableViewCell 14 | 15 | - (void)setFAQ:(ABXFaq*)faq; 16 | 17 | + (CGFloat)heightForFAQ:(ABXFaq*)faq withWidth:(CGFloat)width; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Example/Sample Project/Sample Project-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_3_0 10 | #warning "This project uses features only available in iOS SDK 3.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #import 16 | 17 | #import "ABX.h" 18 | #endif 19 | -------------------------------------------------------------------------------- /Classes/Views/ABXVersionTableViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXVersionTableViewCell.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 22/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class ABXVersion; 12 | 13 | @interface ABXVersionTableViewCell : UITableViewCell 14 | 15 | @property (nonatomic, strong) ABXVersion *version; 16 | 17 | + (CGFloat)heightForVersion:(ABXVersion*)version withWidth:(CGFloat)width; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Classes/Classes/NSDictionary+ABXNSNullAsNull.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+ABXNSNullAsNull.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "NSDictionary+ABXNSNullAsNull.h" 9 | 10 | @implementation NSDictionary (ABXNSNullAsNull) 11 | 12 | - (id)objectForKeyNulled:(id)aKey 13 | { 14 | id value = [self objectForKey:aKey]; 15 | if (!value || [value isKindOfClass:[NSNull class]]) { 16 | return nil; 17 | } 18 | return value; 19 | } 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Classes/Models/ABXAttachment.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXAttachment.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 25/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXModel.h" 10 | 11 | @interface ABXAttachment : ABXModel 12 | 13 | @property (nonatomic, strong) UIImage *image; 14 | @property (nonatomic, copy) NSNumber *identifier; 15 | @property (nonatomic, assign) NSUInteger retries; 16 | 17 | - (NSURLSessionDataTask*)upload:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXFAQViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXFAQViewController.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @class ABXFaq; 11 | 12 | @interface ABXFAQViewController : UIViewController 13 | 14 | @property (nonatomic, strong) ABXFaq *faq; 15 | @property (nonatomic, assign) BOOL hideContactButton; 16 | 17 | + (void)pushOnNavController:(UINavigationController*)navigationController faq:(ABXFaq*)faq hideContactButton:(BOOL)hideContactButton; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Classes/Models/ABXIssue.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXIssue.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXModel.h" 9 | 10 | @interface ABXIssue : ABXModel 11 | 12 | + (NSURLSessionDataTask*)submit:(NSString*)email 13 | feedback:(NSString*)feedback 14 | attachments:(NSArray*)attachments 15 | metaData:(NSDictionary*)metaData 16 | complete:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 25 | # 26 | # Pods/ 27 | 28 | -------------------------------------------------------------------------------- /Classes/Views/ABXNotificationTableViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXNotificationTableViewCell.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 18/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "ABXNotification.h" 12 | #import "ABXVersion.h" 13 | #import "ABXVersionsViewController.h" 14 | 15 | @interface ABXNotificationTableViewCell : UITableViewCell 16 | 17 | @property (nonatomic, strong) ABXNotification *notification; 18 | 19 | + (CGFloat)heightForNotification:(ABXNotification*)notification withWidth:(CGFloat)width; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXBaseListViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXBaseListViewController.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 22/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ABXBaseListViewController : UIViewController 12 | 13 | @property (nonatomic, strong) UITableView *tableView; 14 | @property (nonatomic, strong) UIActivityIndicatorView *activityView; 15 | @property (nonatomic, strong) UILabel *errorLabel; 16 | 17 | + (void)showFromController:(UIViewController*)controller; 18 | 19 | - (void)showError:(NSString*)error; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Classes/Models/ABXModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXModel.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "ABXApiClient.h" 11 | 12 | // Protected methods 13 | #define PROTECTED_ABXMODEL \ 14 | @interface ABXModel () \ 15 | + (id)createWithAttributes:(NSDictionary*)attributes; \ 16 | + (NSURLSessionDataTask*)fetchList:(NSString*)path \ 17 | params:(NSDictionary*)params \ 18 | complete:(void(^)(NSArray *objects, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; \ 19 | @end \ 20 | 21 | @interface ABXModel : NSObject 22 | @end 23 | -------------------------------------------------------------------------------- /Classes/Views/ABXVersionNotificationView.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXVersionNotificationView.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 18/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXNotificationView.h" 10 | 11 | @interface ABXVersionNotificationView : ABXNotificationView 12 | 13 | + (void)fetchAndShowInController:(UIViewController*)controller 14 | foriTunesID:(NSString*)itunesId 15 | backgroundColor:(UIColor*)backgroundColor 16 | textColor:(UIColor*)textColor 17 | buttonColor:(UIColor*)buttonColor 18 | complete:(void(^)(BOOL shown))complete; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXNavigationController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXNavigationController.m 3 | // Realtime 4 | // 5 | // Created by Stuart Hall on 11/06/2014. 6 | // Copyright (c) 2014 Stuart Hall. All rights reserved. 7 | // 8 | 9 | #import "ABXNavigationController.h" 10 | 11 | @interface ABXNavigationController () 12 | 13 | @end 14 | 15 | @implementation ABXNavigationController 16 | 17 | - (void)viewDidLoad 18 | { 19 | [super viewDidLoad]; 20 | } 21 | 22 | - (void)didReceiveMemoryWarning 23 | { 24 | [super didReceiveMemoryWarning]; 25 | // Dispose of any resources that can be recreated. 26 | } 27 | 28 | - (UIStatusBarStyle)preferredStatusBarStyle 29 | { 30 | return UIStatusBarStyleDefault; 31 | } 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXFAQsViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXFAQsViewController.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "ABXBaseListViewController.h" 11 | 12 | @interface ABXFAQsViewController : ABXBaseListViewController 13 | 14 | + (void)showFromController:(UIViewController*)controller 15 | hideContactButton:(BOOL)hideContactButton 16 | contactMetaData:(NSDictionary*)contactMetaData 17 | initialSearch:(NSString*)initialSearch; 18 | 19 | @property (nonatomic, assign) BOOL hideContactButton; 20 | @property (nonatomic, strong) NSDictionary *contactMetaData; 21 | @property (nonatomic, copy) NSString *initialSearch; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /Classes/Views/ABXPromptView.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXPromptView.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 30/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol ABXPromptViewDelegate 12 | 13 | - (void)appbotPromptLiked; 14 | - (void)appbotPromptDidntLike; 15 | - (void)appbotPromptForReview; 16 | - (void)appbotPromptForFeedback; 17 | - (void)appbotPromptClose; 18 | 19 | @end 20 | 21 | @interface ABXPromptView : UIView 22 | 23 | @property (weak) id delegate; 24 | @property (nonatomic, strong) UILabel *label; 25 | @property (nonatomic, strong) UIButton *leftButton; 26 | @property (nonatomic, strong) UIButton *rightButton; 27 | 28 | + (BOOL)hasHadInteractionForCurrentVersion; 29 | 30 | @end -------------------------------------------------------------------------------- /Classes/Classes/NSDictionary+ABXQueryString.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+ABXQueryString.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "NSDictionary+ABXQueryString.h" 9 | 10 | #import "NSString+ABXURLEncoding.h" 11 | 12 | @implementation NSDictionary (ABXQueryString) 13 | 14 | - (NSString *)queryStringValue 15 | { 16 | NSMutableArray *pairs = [NSMutableArray array]; 17 | for (NSString *key in [self keyEnumerator]) 18 | { 19 | id value = [self objectForKey:key]; 20 | NSString *escapedValue = [value urlEncodedString]; 21 | [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, escapedValue]]; 22 | } 23 | 24 | return [pairs componentsJoinedByString:@"&"]; 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Classes/Models/ABXNotification.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXNotification.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "ABXModel.h" 11 | 12 | @interface ABXNotification : ABXModel 13 | 14 | @property (nonatomic, copy) NSNumber *identifier; 15 | @property (nonatomic, copy) NSString *message; 16 | @property (nonatomic, copy) NSString *actionLabel; 17 | @property (nonatomic, copy) NSString *actionUrl; 18 | @property (nonatomic, copy) NSDate *createdAt; 19 | 20 | - (void)markAsSeen; 21 | - (BOOL)hasSeen; 22 | - (BOOL)hasAction; 23 | 24 | + (NSURLSessionDataTask*)fetchActive:(void(^)(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; 25 | 26 | + (NSURLSessionDataTask*)fetch:(void(^)(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Classes/Models/ABXFaq.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXFaq.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "ABXModel.h" 11 | 12 | @interface ABXFaq : ABXModel 13 | 14 | @property (nonatomic, copy) NSNumber *identifier; 15 | @property (nonatomic, copy) NSString *question; 16 | @property (nonatomic, copy) NSString *answer; 17 | 18 | + (NSURLSessionDataTask*)fetch:(void(^)(NSArray *faqs, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; 19 | 20 | - (NSURLSessionDataTask*)upvote:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; 21 | - (NSURLSessionDataTask*)downvote:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; 22 | 23 | - (NSURLSessionDataTask*)recordView:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Classes/Models/ABXVersion.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXVersion.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import "ABXModel.h" 11 | 12 | @interface ABXVersion : ABXModel 13 | 14 | @property (nonatomic, strong) NSDate *releaseDate; 15 | @property (nonatomic, strong) NSString *text; 16 | @property (nonatomic, strong) NSString *version; 17 | 18 | - (void)markAsSeen; 19 | - (BOOL)hasSeen; 20 | - (BOOL)isNewerThanCurrent; 21 | - (void)isLiveVersion:(NSString*)itunesId country:(NSString*)country complete:(void(^)(BOOL matches))complete; 22 | 23 | + (NSURLSessionDataTask*)fetch:(void(^)(NSArray *versions, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; 24 | 25 | + (NSURLSessionDataTask*)fetchCurrentVersion:(void(^)(ABXVersion *currentVersion, ABXVersion *latestVersion, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Classes/Models/ABXAttachment.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXAttachment.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 25/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXAttachment.h" 10 | 11 | #import "NSDictionary+ABXNSNullAsNull.h" 12 | 13 | @implementation ABXAttachment 14 | 15 | - (NSURLSessionDataTask*)upload:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 16 | { 17 | return [[ABXApiClient instance] POSTImage:@"attachments.json" 18 | image:self.image 19 | complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { 20 | self.identifier = [JSON objectForKeyNulled:@"attachment_id"]; 21 | if (complete) { 22 | complete(responseCode, httpCode, error); 23 | } 24 | }]; 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Appbot 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Example/Sample Project/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "40x40", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "60x60", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "29x29", 21 | "scale" : "1x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "29x29", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "40x40", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "40x40", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "76x76", 41 | "scale" : "1x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "76x76", 46 | "scale" : "2x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /WordPress-AppbotX.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint NAME.podspec' to ensure this is a 3 | # valid spec and remove all comments before submitting the spec. 4 | # 5 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 6 | # 7 | Pod::Spec.new do |s| 8 | s.name = "WordPress-AppbotX" 9 | s.version = "1.0.7" 10 | s.summary = "AppbotX is an Obj-C lib for the Appbot server." 11 | s.description = "AppbotX is an iOS client library and sample application for the AppbotX service." 12 | s.homepage = "http://appbot.co" 13 | s.license = 'MIT' 14 | s.author = { "Stuart Hall" => "stuartkhall@gmail.com" } 15 | s.source = { :git => "https://github.com/wordpress-mobile/appbotx.git", :tag => s.version.to_s } 16 | s.social_media_url = 'https://twitter.com/stuartkhall' 17 | s.resources = 'Classes/AppbotX.bundle' 18 | 19 | s.platform = :ios, '6.0' 20 | s.requires_arc = true 21 | 22 | s.source_files = 'Classes/*.{h,m}', 'Classes/Models/*.{h,m}', 'Classes/Views/*.{h,m}', 'Classes/Controllers/*.{h,m}', 'Classes/Classes/*.{h,m}' 23 | end 24 | -------------------------------------------------------------------------------- /Classes/Classes/UIViewController+ABXScreenshot.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+ABXScreenshot.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 30/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "UIViewController+ABXScreenshot.h" 10 | 11 | @implementation UIViewController (ABXScreenshot) 12 | 13 | - (UIImage*)takeScreenshot 14 | { 15 | if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) 16 | UIGraphicsBeginImageContextWithOptions(self.view.window.bounds.size, NO, [UIScreen mainScreen].scale); 17 | else 18 | UIGraphicsBeginImageContext(self.view.window.bounds.size); 19 | 20 | if ([self.view.window respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) { 21 | // iOS 7 22 | [self.view.window drawViewHierarchyInRect:self.view.window.bounds afterScreenUpdates:YES]; 23 | } 24 | else { 25 | // Old school 26 | [self.view.window.layer renderInContext:UIGraphicsGetCurrentContext()]; 27 | } 28 | 29 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 30 | UIGraphicsEndImageContext(); 31 | return image; 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /Classes/Views/ABXNotificationView.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXNotificationView.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 30/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class ABXNotificationView; 12 | 13 | typedef void (^ABXNotificationViewCallback)(ABXNotificationView *view); 14 | 15 | @interface ABXNotificationView : UIView 16 | 17 | + (ABXNotificationView*)show:(NSString*)text 18 | actionText:(NSString*)actionText 19 | backgroundColor:(UIColor*)backgroundColor 20 | textColor:(UIColor*)textColor 21 | buttonColor:(UIColor*)buttonColor 22 | inController:(UIViewController*)controller 23 | actionBlock:(ABXNotificationViewCallback)actionBlock 24 | dismissBlock:(ABXNotificationViewCallback)dismissBlock; 25 | 26 | + (void)fetchAndShowInController:(UIViewController*)controller 27 | backgroundColor:(UIColor*)backgroundColor 28 | textColor:(UIColor*)textColor 29 | buttonColor:(UIColor*)buttonColor 30 | complete:(void(^)(BOOL shown))complete; 31 | 32 | - (void)dismiss; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXFeedbackViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXFeedbackViewController.h 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 30/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol ABXFeedbackViewControllerDelegate 12 | 13 | - (void)abxFeedbackDidSendFeedback; 14 | - (void)abxFeedbackDidntSendFeedback; 15 | 16 | @end 17 | 18 | @interface ABXFeedbackViewController : UIViewController 19 | 20 | @property (nonatomic, copy) NSString *placeholder; 21 | 22 | + (void)showFromController:(UIViewController*)controller 23 | placeholder:(NSString*)placeholder 24 | delegate:(id)delegate; 25 | 26 | + (void)showFromController:(UIViewController*)controller 27 | placeholder:(NSString*)placeholder 28 | email:(NSString*)email 29 | metaData:(NSDictionary*)metaData 30 | image:(UIImage*)image 31 | delegate:(id)delegate; 32 | 33 | @property (nonatomic, copy) NSString *defaultEmail; 34 | @property (nonatomic, strong) NSDictionary *metaData; 35 | @property (nonatomic, strong) UIImage *image; 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /Classes/Classes/NSString+ABXURLEncoding.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+ABXURLEncoding.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "NSString+ABXURLEncoding.h" 9 | 10 | @implementation NSString (ABXURLEncoding) 11 | 12 | - (NSString *)urlEncodedString 13 | { 14 | CFStringRef ref = CFURLCreateStringByAddingPercentEscapes(NULL, 15 | (__bridge CFStringRef)self, 16 | NULL, 17 | (CFStringRef)@":!*();@/&?#[]+$,='%’\"", 18 | kCFStringEncodingUTF8); 19 | return (__bridge_transfer NSString *)(ref); 20 | } 21 | 22 | - (NSString *)urlDecodedString 23 | { 24 | CFStringRef ref = CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, 25 | (__bridge CFStringRef)self, 26 | CFSTR(""), 27 | kCFStringEncodingUTF8); 28 | return (__bridge_transfer NSString *)(ref); 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /Example/Sample Project/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "orientation" : "portrait", 20 | "idiom" : "ipad", 21 | "extent" : "full-screen", 22 | "minimum-system-version" : "7.0", 23 | "scale" : "1x" 24 | }, 25 | { 26 | "orientation" : "landscape", 27 | "idiom" : "ipad", 28 | "extent" : "full-screen", 29 | "minimum-system-version" : "7.0", 30 | "scale" : "1x" 31 | }, 32 | { 33 | "orientation" : "portrait", 34 | "idiom" : "ipad", 35 | "extent" : "full-screen", 36 | "minimum-system-version" : "7.0", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "orientation" : "landscape", 41 | "idiom" : "ipad", 42 | "extent" : "full-screen", 43 | "minimum-system-version" : "7.0", 44 | "scale" : "2x" 45 | } 46 | ], 47 | "info" : { 48 | "version" : 1, 49 | "author" : "xcode" 50 | } 51 | } -------------------------------------------------------------------------------- /Classes/Classes/ABXAppStore.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXAppStore.m 3 | // 4 | // Created by Stuart Hall on 18/06/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXAppStore.h" 9 | 10 | @implementation ABXAppStore 11 | 12 | + (void)openAppStoreReviewForApp:(NSString*)itunesId 13 | { 14 | if ([[[UIDevice currentDevice] systemVersion] compare:@"7.1" options:NSNumericSearch] != NSOrderedAscending) { 15 | // Since 7.1 we can throw to the review tab 16 | 17 | // NOTE: 18 | // Updating iTunes URL, since this broke in Simplenote. 19 | // 20 | // NSString *url = [NSString stringWithFormat:@"http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=%@&pageNumber=0&ct=appbotReviewPrompt&at=11l4LZ&type=Purple%%252BSoftware&mt=8&sortOrdering=2", itunesId]; 21 | // 22 | 23 | NSString *url = [NSString stringWithFormat:@"http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=%@&pageNumber=0&sortOrdering=2&type=Purple+Software&mt=8", itunesId]; 24 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; 25 | } 26 | else { 27 | [self openAppStoreForApp:itunesId]; 28 | } 29 | } 30 | 31 | + (void)openAppStoreForApp:(NSString*)itunesId 32 | { 33 | NSString *url = [NSString stringWithFormat:@"https://itunes.apple.com/au/app/app/id%@?mt=8", itunesId]; 34 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /Classes/Classes/ABXApiClient.h: -------------------------------------------------------------------------------- 1 | // 2 | // ABXApiClient.h 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | // Error codes 11 | typedef enum { 12 | ABXResponseCodeSuccess, // Request completed successfully 13 | ABXResponseCodeErrorAuth, // Check your bundle identifier and API key 14 | ABXResponseCodeErrorExpired, // Account requires payment 15 | ABXResponseCodeErrorDecoding, // Error decoding the JSON data 16 | ABXResponseCodeErrorEncoding, // Error encoding the post/put request 17 | ABXResponseCodeErrorNotFound, // Not found 18 | ABXResponseCodeErrorUnknown // Unknown error 19 | } ABXResponseCode; 20 | 21 | typedef void (^ABXRequestCompletion)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON); 22 | 23 | @interface ABXApiClient : NSObject 24 | 25 | + (ABXApiClient*)instance; 26 | 27 | + (BOOL)isInternetReachable; 28 | 29 | - (void)setApiKey:(NSString *)apiKey; 30 | 31 | - (NSURLSessionDataTask*)GET:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete; 32 | 33 | - (NSURLSessionDataTask*)POST:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete; 34 | - (NSURLSessionDataTask*)POSTImage:(NSString*)path image:(UIImage*)image complete:(ABXRequestCompletion)complete; 35 | 36 | - (NSURLSessionDataTask*)PUT:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /Example/Sample Project/Sample Project-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | co.appbot.sampleproject 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.2 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 2 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | Launch Screen 29 | UIMainStoryboardFile 30 | Storyboard 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UIStatusBarHidden 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Classes/Views/ABXFAQTableViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXFAQTableViewCell.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 15/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXFAQTableViewCell.h" 10 | 11 | #import "NSString+ABXSizing.h" 12 | #import "ABXFaq.h" 13 | 14 | @interface ABXFAQTableViewCell () 15 | 16 | @property (nonatomic, strong) UILabel *questionLabel; 17 | 18 | @property (nonatomic, strong) ABXFaq *faq; 19 | 20 | @end 21 | 22 | @implementation ABXFAQTableViewCell 23 | 24 | - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 25 | { 26 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 27 | if (self) { 28 | self.layer.shouldRasterize = YES; 29 | self.layer.rasterizationScale = [UIScreen mainScreen].scale; 30 | self.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 31 | 32 | // Text 33 | self.questionLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 20, CGRectGetWidth(self.bounds) - 45, 0)]; 34 | self.questionLabel.textColor = [UIColor blackColor]; 35 | self.questionLabel.font = [ABXFAQTableViewCell font]; 36 | self.questionLabel.numberOfLines = 0; 37 | [self.contentView addSubview:self.questionLabel]; 38 | } 39 | return self; 40 | } 41 | 42 | - (void)layoutSubviews 43 | { 44 | [super layoutSubviews]; 45 | 46 | CGRect r = self.questionLabel.frame; 47 | r.size.height = [_faq.question heightForWidth:CGRectGetWidth(self.bounds) - 45 48 | andFont:[ABXFAQTableViewCell font]]; 49 | r.size.width = CGRectGetWidth(self.bounds) - 45; 50 | self.questionLabel.frame = r; 51 | } 52 | 53 | - (void)setFAQ:(ABXFaq*)faq 54 | { 55 | _faq = faq; 56 | self.questionLabel.text = faq.question; 57 | [self setNeedsLayout]; 58 | } 59 | 60 | + (UIFont*)font 61 | { 62 | static dispatch_once_t onceToken; 63 | static UIFont *font = nil; 64 | dispatch_once(&onceToken, ^{ 65 | font = [UIFont systemFontOfSize:15]; 66 | }); 67 | return font; 68 | } 69 | 70 | + (CGFloat)heightForFAQ:(ABXFaq*)faq withWidth:(CGFloat)width 71 | { 72 | return [faq.question heightForWidth:width - 45 andFont:[self font]] + 40; 73 | } 74 | 75 | @end 76 | -------------------------------------------------------------------------------- /Example/Sample Project/ABXAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXAppDelegate.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 21/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXAppDelegate.h" 10 | 11 | @implementation ABXAppDelegate 12 | 13 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 14 | { 15 | // The test API key, you will need to change this to suit 16 | [[ABXApiClient instance] setApiKey:@"bc2c1345090cee2262258834db71a1e9417365a7"]; 17 | 18 | if ([self.window respondsToSelector:@selector(setTintColor:)]) { 19 | self.window.tintColor = [UIColor colorWithRed:0xf5/255.0f green:0x8c/255.0f blue:0x75/255.0f alpha:1]; 20 | } 21 | [self.window makeKeyAndVisible]; 22 | return YES; 23 | } 24 | 25 | - (void)applicationWillResignActive:(UIApplication *)application 26 | { 27 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 28 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 29 | } 30 | 31 | - (void)applicationDidEnterBackground:(UIApplication *)application 32 | { 33 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 34 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 35 | } 36 | 37 | - (void)applicationWillEnterForeground:(UIApplication *)application 38 | { 39 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 40 | } 41 | 42 | - (void)applicationDidBecomeActive:(UIApplication *)application 43 | { 44 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 45 | } 46 | 47 | - (void)applicationWillTerminate:(UIApplication *)application 48 | { 49 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 50 | } 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /Classes/Classes/NSString+ABXSizing.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+ABXSizing.m 3 | // 4 | // Created by Stuart Hall on 12/06/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "NSString+ABXSizing.h" 9 | 10 | @implementation NSString (ABXSizing) 11 | 12 | - (CGFloat)heightForWidth:(CGFloat)width andFont:(UIFont*)font 13 | { 14 | CGSize size; 15 | CGSize constraintSize = CGSizeMake(width, CGFLOAT_MAX); 16 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 17 | if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] != NSOrderedAscending) { 18 | size = [self boundingRectWithSize:constraintSize 19 | options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading 20 | attributes:@{NSFontAttributeName:font} 21 | context:nil].size; 22 | } 23 | else { 24 | #pragma clang diagnostic push 25 | #pragma clang diagnostic ignored "-Wdeprecated" 26 | 27 | size = [self sizeWithFont:font 28 | constrainedToSize:constraintSize 29 | lineBreakMode:NSLineBreakByWordWrapping]; 30 | 31 | #pragma clang diagnostic pop 32 | } 33 | #else 34 | size = [self sizeWithFont:font 35 | constrainedToSize:constraintSize 36 | lineBreakMode:UILineBreakModeWordWrap]; 37 | #endif 38 | 39 | return ceil(size.height); 40 | } 41 | 42 | - (CGFloat)widthToFitFont:(UIFont*)font 43 | { 44 | CGSize size; 45 | CGSize constraintSize = CGSizeMake(CGFLOAT_MAX, font.lineHeight); 46 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 47 | if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] != NSOrderedAscending) { 48 | size = [self boundingRectWithSize:constraintSize 49 | options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading 50 | attributes:@{NSFontAttributeName:font} 51 | context:nil].size; 52 | } 53 | else { 54 | #pragma clang diagnostic push 55 | #pragma clang diagnostic ignored "-Wdeprecated" 56 | 57 | size = [self sizeWithFont:font 58 | constrainedToSize:constraintSize 59 | lineBreakMode:NSLineBreakByTruncatingTail]; 60 | 61 | #pragma clang diagnostic pop 62 | } 63 | #else 64 | size = [self sizeWithFont:font 65 | constrainedToSize:constraintSize 66 | lineBreakMode:UILineBreakByTruncatingTail]; 67 | #endif 68 | 69 | return ceil(size.width); 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /Classes/Models/ABXModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXModel.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXModel.h" 9 | 10 | #import "NSDictionary+ABXNSNullAsNull.h" 11 | 12 | @implementation ABXModel 13 | 14 | + (id)createWithAttributes:(NSDictionary*)attributes 15 | { 16 | // Virtual 17 | assert(false); 18 | } 19 | 20 | + (NSURLSessionDataTask*)fetchList:(NSString*)path 21 | params:(NSDictionary*)params 22 | complete:(void(^)(NSArray *objects, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 23 | { 24 | // Generic list fetch 25 | return [[ABXApiClient instance] GET:path 26 | params:params 27 | complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { 28 | if (responseCode == ABXResponseCodeSuccess) { 29 | NSArray *results = [JSON objectForKeyNulled:@"results"]; 30 | if (results && [results isKindOfClass:[NSArray class]]) { 31 | // Convert into objects 32 | NSMutableArray *objects = [NSMutableArray arrayWithCapacity:[results count]]; 33 | for (NSDictionary *attrs in results) { 34 | if ([attrs isKindOfClass:[NSDictionary class]]) { 35 | [objects addObject:[self createWithAttributes:attrs]]; 36 | } 37 | } 38 | 39 | // Success! 40 | if (complete) { 41 | complete(objects, responseCode, httpCode, error); 42 | } 43 | } 44 | else { 45 | // Decoding error, pass the values through 46 | if (complete) { 47 | complete(nil, ABXResponseCodeErrorDecoding, httpCode, error); 48 | } 49 | } 50 | } 51 | else { 52 | // Error, pass the values through 53 | if (complete) { 54 | complete(nil, responseCode, httpCode, error); 55 | } 56 | } 57 | }]; 58 | } 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /Classes/Classes/ABXKeychain.h: -------------------------------------------------------------------------------- 1 | // 2 | // Altered Version of https://github.com/nicklockwood/FXKeychain 3 | // Altered to not conflict with any existing uses of the library 4 | // 5 | // FXKeychain.h 6 | // 7 | // Version 1.5 beta 8 | // 9 | // Created by Nick Lockwood on 29/12/2012. 10 | // Copyright 2012 Charcoal Design 11 | // 12 | // Distributed under the permissive zlib License 13 | // Get the latest version from here: 14 | // 15 | // https://github.com/nicklockwood/FXKeychain 16 | // 17 | // This software is provided 'as-is', without any express or implied 18 | // warranty. In no event will the authors be held liable for any damages 19 | // arising from the use of this software. 20 | // 21 | // Permission is granted to anyone to use this software for any purpose, 22 | // including commercial applications, and to alter it and redistribute it 23 | // freely, subject to the following restrictions: 24 | // 25 | // 1. The origin of this software must not be misrepresented; you must not 26 | // claim that you wrote the original software. If you use this software 27 | // in a product, an acknowledgment in the product documentation would be 28 | // appreciated but is not required. 29 | // 30 | // 2. Altered source versions must be plainly marked as such, and must not be 31 | // misrepresented as being the original software. 32 | // 33 | // 3. This notice may not be removed or altered from any source distribution. 34 | // 35 | 36 | #import 37 | 38 | #import 39 | 40 | 41 | #pragma GCC diagnostic push 42 | #pragma GCC diagnostic ignored "-Wobjc-missing-property-synthesis" 43 | 44 | 45 | #ifndef APPBOTKEYCHAIN_USE_NSCODING 46 | #if TARGET_OS_IPHONE 47 | #define APPBOTKEYCHAIN_USE_NSCODING 1 48 | #else 49 | #define APPBOTKEYCHAIN_USE_NSCODING 0 50 | #endif 51 | #endif 52 | 53 | 54 | typedef NS_ENUM(NSInteger, ABXKeychainAccess) 55 | { 56 | ABXKeychainAccessibleWhenUnlocked = 0, 57 | ABXKeychainAccessibleAfterFirstUnlock, 58 | ABXKeychainAccessibleAlways, 59 | ABXKeychainAccessibleWhenUnlockedThisDeviceOnly, 60 | ABXKeychainAccessibleAfterFirstUnlockThisDeviceOnly, 61 | ABXKeychainAccessibleAlwaysThisDeviceOnly 62 | }; 63 | 64 | 65 | @interface ABXKeychain : NSObject 66 | 67 | + (instancetype)defaultKeychain; 68 | 69 | @property (nonatomic, readonly) NSString *service; 70 | @property (nonatomic, readonly) NSString *accessGroup; 71 | @property (nonatomic, assign) ABXKeychainAccess accessibility; 72 | 73 | - (id)initWithService:(NSString *)service 74 | accessGroup:(NSString *)accessGroup 75 | accessibility:(ABXKeychainAccess)accessibility; 76 | 77 | - (id)initWithService:(NSString *)service 78 | accessGroup:(NSString *)accessGroup; 79 | 80 | - (BOOL)setObject:(id)object forKey:(id)key; 81 | - (BOOL)setObject:(id)object forKeyedSubscript:(id)key; 82 | - (BOOL)removeObjectForKey:(id)key; 83 | - (id)objectForKey:(id)key; 84 | - (id)objectForKeyedSubscript:(id)key; 85 | 86 | @end 87 | 88 | 89 | #pragma GCC diagnostic pop 90 | -------------------------------------------------------------------------------- /Classes/Views/ABXTextView.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXTextView.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 30/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXTextView.h" 10 | 11 | @interface ABXTextView () 12 | 13 | @property (nonatomic, assign) BOOL didDrawPlaceholder; 14 | 15 | @end 16 | 17 | @implementation ABXTextView 18 | 19 | - (id)initWithCoder:(NSCoder *)aDecoder 20 | { 21 | if ((self = [super initWithCoder:aDecoder])) { 22 | [self setup]; 23 | } 24 | return self; 25 | } 26 | 27 | 28 | - (id)initWithFrame:(CGRect)frame 29 | { 30 | if ((self = [super initWithFrame:frame])) 31 | { 32 | [self setup]; 33 | } 34 | return self; 35 | } 36 | 37 | - (void)setup 38 | { 39 | [[NSNotificationCenter defaultCenter] addObserver:self 40 | selector:@selector(textDidChange:) 41 | name:UITextViewTextDidChangeNotification 42 | object:self]; 43 | [[NSNotificationCenter defaultCenter] addObserver:self 44 | selector:@selector(setNeedsDisplay) 45 | name:UIDeviceOrientationDidChangeNotification 46 | object:nil]; 47 | } 48 | 49 | - (void)textDidChange:(NSNotification*)notification 50 | { 51 | if (self.didDrawPlaceholder || self.text.length == 0) { 52 | [self setNeedsDisplay]; 53 | } 54 | } 55 | 56 | - (void)dealloc 57 | { 58 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 59 | } 60 | 61 | - (void)drawRect:(CGRect)rect 62 | { 63 | [super drawRect:rect]; 64 | 65 | if (self.text.length == 0 && self.placeholder) { 66 | self.didDrawPlaceholder = YES; 67 | if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] != NSOrderedAscending) { 68 | CGRect rect = CGRectInset(self.bounds, 4, 8); 69 | [self.placeholder drawInRect:rect 70 | withAttributes:@{ NSFontAttributeName : self.font, 71 | NSForegroundColorAttributeName : [UIColor colorWithWhite:0.8 alpha:1]}]; 72 | } 73 | else { 74 | #pragma clang diagnostic push 75 | #pragma clang diagnostic ignored "-Wdeprecated" 76 | CGRect rect = CGRectInset(self.bounds, 8, 8); 77 | [[UIColor colorWithWhite:0.8 alpha:1] set]; 78 | [self.placeholder drawInRect:rect withFont:self.font]; 79 | #pragma clang diagnostic pop 80 | } 81 | } 82 | else { 83 | self.didDrawPlaceholder = NO; 84 | } 85 | } 86 | 87 | - (void)setText:(NSString *)text 88 | { 89 | [super setText:text]; 90 | if (self.didDrawPlaceholder || self.text.length == 0) { 91 | [self setNeedsDisplay]; 92 | } 93 | } 94 | 95 | - (void)setPlaceholder:(NSString *)placeholder 96 | { 97 | _placeholder = placeholder; 98 | [self setNeedsDisplay]; 99 | } 100 | 101 | @end 102 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXVersionsViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXVersionsViewController.m 3 | // 4 | // Created by Stuart Hall on 22/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXVersionsViewController.h" 9 | 10 | #import "ABXVersion.h" 11 | #import "ABXVersionTableViewCell.h" 12 | #import "NSString+ABXLocalized.h" 13 | 14 | @interface ABXVersionsViewController () 15 | 16 | @property (nonatomic, strong) NSArray *versions; 17 | 18 | @end 19 | 20 | @implementation ABXVersionsViewController 21 | 22 | - (void)viewDidLoad 23 | { 24 | [super viewDidLoad]; 25 | 26 | self.title = [@"Versions" localizedString]; 27 | 28 | if (![ABXApiClient isInternetReachable]) { 29 | [self.activityView stopAnimating]; 30 | [self showError:[@"No Internet" localizedString]]; 31 | } 32 | else { 33 | [self fetchVersions]; 34 | } 35 | } 36 | 37 | - (void)didReceiveMemoryWarning 38 | { 39 | [super didReceiveMemoryWarning]; 40 | // Dispose of any resources that can be recreated. 41 | } 42 | 43 | #pragma mark - Buttons 44 | 45 | - (void)onDone 46 | { 47 | [self.navigationController dismissViewControllerAnimated:YES completion:nil]; 48 | } 49 | 50 | #pragma mark - Fetching 51 | 52 | - (void)fetchVersions 53 | { 54 | [ABXVersion fetch:^(NSArray *versions, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 55 | [self.activityView stopAnimating]; 56 | if (responseCode == ABXResponseCodeSuccess) { 57 | self.versions = versions; 58 | [self.tableView reloadData]; 59 | 60 | if (versions.count == 0) { 61 | [self showError:[@"No Versions" localizedString]]; 62 | } 63 | } 64 | else { 65 | [self showError:[@"Versions Error" localizedString]]; 66 | } 67 | }]; 68 | } 69 | 70 | #pragma mark - UITableViewDataSource 71 | 72 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 73 | { 74 | return 1; 75 | } 76 | 77 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 78 | { 79 | return self.versions.count; 80 | } 81 | 82 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 83 | { 84 | static NSString *CellIdentifier = @"VersionCell"; 85 | 86 | ABXVersionTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 87 | if (!cell) { 88 | cell = [[ABXVersionTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; 89 | } 90 | 91 | if (indexPath.row < self.versions.count) { 92 | [cell setVersion:self.versions[indexPath.row]]; 93 | } 94 | 95 | return cell; 96 | } 97 | 98 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 99 | { 100 | if (indexPath.row < self.versions.count) { 101 | return [ABXVersionTableViewCell heightForVersion:self.versions[indexPath.row] 102 | withWidth:CGRectGetWidth(self.tableView.frame)]; 103 | } 104 | return 0; 105 | } 106 | 107 | 108 | @end 109 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXNotificationsViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXNotificationsViewController.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 18/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXNotificationsViewController.h" 10 | 11 | #import "ABXNotificationTableViewCell.h" 12 | #import "NSString+ABXLocalized.h" 13 | 14 | @interface ABXNotificationsViewController () 15 | 16 | @property (nonatomic, strong) NSArray *notifications; 17 | 18 | @end 19 | 20 | @implementation ABXNotificationsViewController 21 | 22 | - (void)viewDidLoad 23 | { 24 | [super viewDidLoad]; 25 | 26 | self.title = [@"Notifications" localizedString]; 27 | 28 | if (![ABXApiClient isInternetReachable]) { 29 | [self.activityView stopAnimating]; 30 | [self showError:[@"No Internet" localizedString]]; 31 | } 32 | else { 33 | [self fetchNotifications]; 34 | } 35 | } 36 | 37 | - (void)didReceiveMemoryWarning 38 | { 39 | [super didReceiveMemoryWarning]; 40 | // Dispose of any resources that can be recreated. 41 | } 42 | 43 | #pragma mark - Buttons 44 | 45 | - (void)onDone 46 | { 47 | [self.navigationController dismissViewControllerAnimated:YES completion:nil]; 48 | } 49 | 50 | #pragma mark - Fetching 51 | 52 | - (void)fetchNotifications 53 | { 54 | [ABXNotification fetch:^(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 55 | [self.activityView stopAnimating]; 56 | if (responseCode == ABXResponseCodeSuccess) { 57 | self.notifications = notifications; 58 | [self.tableView reloadData]; 59 | 60 | if (notifications.count == 0) { 61 | [self showError:[@"No Notifications" localizedString]]; 62 | } 63 | } 64 | else { 65 | [self showError:[@"Notifications Error" localizedString]]; 66 | } 67 | }]; 68 | } 69 | 70 | #pragma mark - UITableViewDataSource 71 | 72 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 73 | { 74 | return 1; 75 | } 76 | 77 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 78 | { 79 | return self.notifications.count; 80 | } 81 | 82 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 83 | { 84 | static NSString *CellIdentifier = @"NotificationCell"; 85 | 86 | ABXNotificationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 87 | if (!cell) { 88 | cell = [[ABXNotificationTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; 89 | } 90 | 91 | if (indexPath.row < self.notifications.count) { 92 | [cell setNotification:self.notifications[indexPath.row]]; 93 | } 94 | 95 | return cell; 96 | } 97 | 98 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 99 | { 100 | if (indexPath.row < self.notifications.count) { 101 | return [ABXNotificationTableViewCell heightForNotification:self.notifications[indexPath.row] 102 | withWidth:CGRectGetWidth(self.tableView.frame)]; 103 | } 104 | return 44; 105 | } 106 | 107 | 108 | @end 109 | -------------------------------------------------------------------------------- /Classes/Classes/NSString+ABXLocalized.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+ABXLocalized.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 26/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "NSString+ABXLocalized.h" 10 | 11 | @implementation NSString (ABXLocalized) 12 | 13 | - (NSMutableDictionary*)appbotXBundles 14 | { 15 | static dispatch_once_t onceToken; 16 | static NSMutableDictionary *bundles = nil; 17 | dispatch_once(&onceToken, ^{ 18 | bundles = [NSMutableDictionary dictionary]; 19 | }); 20 | return bundles; 21 | } 22 | 23 | - (NSBundle*)appbotXBundle 24 | { 25 | static dispatch_once_t onceToken; 26 | static NSBundle *bundle = nil; 27 | dispatch_once(&onceToken, ^{ 28 | NSString *path = [[NSBundle mainBundle] pathForResource:@"AppbotX" ofType:@"bundle"]; 29 | bundle = [NSBundle bundleWithPath:path]; 30 | }); 31 | return bundle; 32 | } 33 | 34 | - (NSString*)localizedString 35 | { 36 | // Load our language bundle 37 | static dispatch_once_t onceToken; 38 | static NSBundle *appbotXBundle = nil; 39 | dispatch_once(&onceToken, ^{ 40 | NSString *path = [[NSBundle mainBundle] pathForResource:@"AppbotX" ofType:@"bundle"]; 41 | appbotXBundle = [NSBundle bundleWithPath:path]; 42 | }); 43 | 44 | // Loop through the prefered languages 45 | if ([NSLocale preferredLanguages].count > 0) { 46 | // Only the first language is valuable 47 | // the rest seems to be jibberish 48 | NSString *language = [[[NSLocale preferredLanguages] firstObject] lowercaseString]; 49 | 50 | // First try the language 51 | NSString *s = [self localizedStringForLanguage:language]; 52 | if (s.length > 0) { 53 | return s; 54 | } 55 | 56 | // See if we have the root language e.g. en for en-GB 57 | NSArray *parts = [language componentsSeparatedByString:@"-"]; 58 | if (parts.count > 1) { 59 | NSString *s = [self localizedStringForLanguage:[parts firstObject]]; 60 | if (s.length > 0) { 61 | return s; 62 | } 63 | } 64 | } 65 | 66 | // If we still don't have a localisation then fall back to en 67 | // if all else fails use the key 68 | return [self localizedStringForLanguage:@"en"] ?: self; 69 | } 70 | 71 | - (NSString*)localizedStringForLanguage:(NSString*)language 72 | { 73 | static NSString *kNilValue = @"_na_"; 74 | // First check if the user has defined a localisation 75 | if ([[[NSBundle mainBundle] localizations] containsObject:language]) { 76 | NSString *s = [[NSBundle mainBundle] localizedStringForKey:self value:kNilValue table:nil]; 77 | if (s.length > 0 && ![s isEqualToString:kNilValue]) { 78 | // Use their localisation 79 | return s; 80 | } 81 | } 82 | 83 | // Look if we have a localisation 84 | if ([[[self appbotXBundle] localizations] containsObject:language]) { 85 | NSBundle *bundle = [[self appbotXBundles] objectForKey:language]; 86 | if (!bundle) { 87 | NSString *path = [[self appbotXBundle] pathForResource:language ofType:@"lproj"]; 88 | bundle = [NSBundle bundleWithPath:path]; 89 | if (bundle) { 90 | // Cache the bundle 91 | [[self appbotXBundles] setObject:bundle forKey:language]; 92 | } 93 | } 94 | 95 | if (bundle) { 96 | NSString *s = [bundle localizedStringForKey:self value:kNilValue table:nil]; 97 | if (s.length > 0 && ![s isEqualToString:kNilValue]) { 98 | // Use our localisation 99 | return s; 100 | } 101 | } 102 | } 103 | 104 | return nil; 105 | } 106 | 107 | @end 108 | -------------------------------------------------------------------------------- /Example/Sample Project/Launch Screen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Classes/Views/ABXVersionTableViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXVersionTableViewCell.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 22/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXVersionTableViewCell.h" 10 | 11 | #import "ABXVersion.h" 12 | #import "NSString+ABXSizing.h" 13 | #import "NSString+ABXLocalized.h" 14 | 15 | @interface ABXVersionTableViewCell () 16 | 17 | @property (nonatomic, strong) UILabel *versionLabel; 18 | @property (nonatomic, strong) UILabel *dateLabel; 19 | @property (nonatomic, strong) UILabel *textDetailsLabel; 20 | 21 | @end 22 | 23 | @implementation ABXVersionTableViewCell 24 | 25 | - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 26 | { 27 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 28 | if (self) { 29 | self.layer.shouldRasterize = YES; 30 | self.layer.rasterizationScale = [UIScreen mainScreen].scale; 31 | self.selectionStyle = UITableViewCellSelectionStyleNone; 32 | 33 | // Version number 34 | self.versionLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 10, (CGRectGetWidth(self.contentView.bounds) - 30)/2, 30)]; 35 | self.versionLabel.textColor = [UIColor blackColor]; 36 | self.versionLabel.font = [UIFont systemFontOfSize:15]; 37 | [self.contentView addSubview:self.versionLabel]; 38 | 39 | // Release date 40 | self.dateLabel = [[UILabel alloc] initWithFrame:CGRectMake(CGRectGetMidX(self.contentView.bounds), 10, (CGRectGetWidth(self.contentView.bounds) - 30)/2, 30)]; 41 | self.dateLabel.textColor = [UIColor blackColor]; 42 | self.dateLabel.textAlignment = NSTextAlignmentRight; 43 | self.dateLabel.font = [UIFont systemFontOfSize:15]; 44 | self.dateLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; 45 | [self.contentView addSubview:self.dateLabel]; 46 | 47 | // Text 48 | self.textDetailsLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 40, CGRectGetWidth(self.contentView.bounds) - 30, 0)]; 49 | self.textDetailsLabel.textColor = [UIColor darkGrayColor]; 50 | self.textDetailsLabel.font = [ABXVersionTableViewCell detailFont]; 51 | self.textDetailsLabel.numberOfLines = 0; 52 | self.textDetailsLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; 53 | [self.contentView addSubview:self.textDetailsLabel]; 54 | } 55 | return self; 56 | } 57 | 58 | - (void)setVersion:(ABXVersion *)version 59 | { 60 | _version = version; 61 | 62 | self.versionLabel.text = [[[@"Version" localizedString] stringByAppendingString:@" "] stringByAppendingString:version.version]; 63 | 64 | static dispatch_once_t onceToken; 65 | static NSDateFormatter *dateFormatter = nil; 66 | dispatch_once(&onceToken, ^{ 67 | dateFormatter = [[NSDateFormatter alloc] init]; 68 | [dateFormatter setDateStyle:NSDateFormatterLongStyle]; 69 | }); 70 | 71 | self.dateLabel.text = [dateFormatter stringFromDate:version.releaseDate]; 72 | 73 | self.textDetailsLabel.text = version.text; 74 | [self setNeedsLayout]; 75 | } 76 | 77 | - (void)layoutSubviews 78 | { 79 | [super layoutSubviews]; 80 | 81 | CGRect r = self.textDetailsLabel.frame; 82 | r.size.height = [self.version.text heightForWidth:CGRectGetWidth(self.contentView.bounds) - 30 andFont:[ABXVersionTableViewCell detailFont]]; 83 | self.textDetailsLabel.frame = r; 84 | } 85 | 86 | + (UIFont*)detailFont 87 | { 88 | static dispatch_once_t onceToken; 89 | static UIFont *font = nil; 90 | dispatch_once(&onceToken, ^{ 91 | font = [UIFont systemFontOfSize:14]; 92 | }); 93 | return font; 94 | } 95 | 96 | + (CGFloat)heightForVersion:(ABXVersion*)version withWidth:(CGFloat)width 97 | { 98 | NSLog(@"- Width : %f", width); 99 | 100 | return [version.text heightForWidth:width - 30 andFont:[self detailFont]] + 60; 101 | } 102 | 103 | @end 104 | -------------------------------------------------------------------------------- /Classes/Models/ABXNotification.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXNotification.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXNotification.h" 9 | 10 | #import "NSDictionary+ABXNSNullAsNull.h" 11 | 12 | PROTECTED_ABXMODEL 13 | 14 | @implementation ABXNotification 15 | 16 | - (id)initWithAttributes:(NSDictionary*)attributes 17 | { 18 | self = [super init]; 19 | if (self) { 20 | self.identifier = [attributes objectForKeyNulled:@"id"]; 21 | 22 | // Date formatter, cache as they are expensive to create 23 | static dispatch_once_t onceToken; 24 | static NSDateFormatter *formatter = nil; 25 | dispatch_once(&onceToken, ^{ 26 | formatter = [NSDateFormatter new]; 27 | [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZ"]; 28 | }); 29 | 30 | NSString *createdAtString = [attributes objectForKeyNulled:@"created_at"]; 31 | if (createdAtString != nil) { 32 | self.createdAt = [formatter dateFromString:createdAtString]; 33 | } 34 | 35 | // Look for a matching localisation 36 | if ([NSLocale preferredLanguages].count > 0) { 37 | NSString *language = [[NSLocale preferredLanguages] firstObject]; 38 | 39 | // Make sure we don't match the default language code 40 | NSString *defaultLanguage = [attributes valueForKeyPath:@"language.code"]; 41 | if (!defaultLanguage || ![language caseInsensitiveCompare:defaultLanguage] == NSOrderedSame) { 42 | // Look for an exact match 43 | [self lookForLocalisation:attributes language:language]; 44 | 45 | // Look for a partial match (e.g. match en to en-au) 46 | if (self.message == nil) { 47 | NSArray *parts = [language componentsSeparatedByString:@"-"]; 48 | if (parts.count > 1) { 49 | language = [parts firstObject]; 50 | [self lookForLocalisation:attributes language:language]; 51 | } 52 | } 53 | } 54 | } 55 | 56 | // Fall back to the default if we have nothing 57 | if (self.message == nil) { 58 | self.message = [attributes objectForKeyNulled:@"message"]; 59 | self.actionLabel = [attributes objectForKeyNulled:@"action_label"]; 60 | self.actionUrl = [attributes objectForKeyNulled:@"action_url"]; 61 | } 62 | } 63 | return self; 64 | } 65 | 66 | + (id)createWithAttributes:(NSDictionary*)attributes 67 | { 68 | return [[ABXNotification alloc] initWithAttributes:attributes]; 69 | } 70 | 71 | - (void)lookForLocalisation:(NSDictionary*)attributes language:(NSString*)language 72 | { 73 | for (NSDictionary *localisation in [attributes objectForKeyNulled:@"localizations"]) { 74 | NSString *languageCode = [localisation valueForKeyPath:@"language.code"]; 75 | if (languageCode && [languageCode caseInsensitiveCompare:language] == NSOrderedSame) { 76 | // Matching localisation 77 | self.message = [localisation objectForKeyNulled:@"message"]; 78 | self.actionLabel = [localisation objectForKeyNulled:@"action_label"]; 79 | self.actionUrl = [localisation objectForKeyNulled:@"action_url"]; 80 | break; 81 | } 82 | } 83 | } 84 | 85 | + (NSURLSessionDataTask*)fetchActive:(void(^)(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 86 | { 87 | return [self fetchList:@"notifications/active" params:nil complete:complete]; 88 | } 89 | 90 | + (NSURLSessionDataTask*)fetch:(void(^)(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 91 | { 92 | return [self fetchList:@"notifications" params:nil complete:complete]; 93 | } 94 | 95 | - (BOOL)hasAction 96 | { 97 | return self.actionUrl.length > 0 && self.actionLabel.length > 0; 98 | } 99 | 100 | - (void)markAsSeen 101 | { 102 | [[NSUserDefaults standardUserDefaults] setBool:YES forKey:[@"Notification" stringByAppendingString:[self.identifier stringValue]]]; 103 | [[NSUserDefaults standardUserDefaults] synchronize]; 104 | } 105 | 106 | - (BOOL)hasSeen 107 | { 108 | return [[NSUserDefaults standardUserDefaults] boolForKey:[@"Notification" stringByAppendingString:[self.identifier stringValue]]]; 109 | } 110 | 111 | @end 112 | -------------------------------------------------------------------------------- /Classes/Models/ABXFaq.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXFaq.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXFaq.h" 9 | 10 | #import "NSDictionary+ABXNSNullAsNull.h" 11 | 12 | PROTECTED_ABXMODEL 13 | 14 | @implementation ABXFaq 15 | 16 | - (id)initWithAttributes:(NSDictionary*)attributes 17 | { 18 | self = [super init]; 19 | if (self) { 20 | self.identifier = [attributes objectForKeyNulled:@"id"]; 21 | 22 | // Look for a matching localisation 23 | if ([NSLocale preferredLanguages].count > 0) { 24 | NSString *language = [[NSLocale preferredLanguages] firstObject]; 25 | 26 | // Make sure we don't match the default language code 27 | NSString *defaultLanguage = [attributes valueForKeyPath:@"language.code"]; 28 | if (!defaultLanguage || ![language caseInsensitiveCompare:defaultLanguage] == NSOrderedSame) { 29 | // Look for an exact match 30 | [self lookForLocalisation:attributes language:language]; 31 | 32 | // Look for a partial match (e.g. match en to en-au) 33 | if (self.question == nil || self.answer == nil) { 34 | NSArray *parts = [language componentsSeparatedByString:@"-"]; 35 | if (parts.count > 1) { 36 | language = [parts firstObject]; 37 | [self lookForLocalisation:attributes language:language]; 38 | } 39 | } 40 | } 41 | } 42 | 43 | // Fall back to the default if we have nothing 44 | if (self.question == nil || self.answer == nil) { 45 | self.question = [attributes objectForKeyNulled:@"question"]; 46 | self.answer = [attributes objectForKeyNulled:@"answer"]; 47 | } 48 | } 49 | 50 | return self; 51 | } 52 | 53 | + (id)createWithAttributes:(NSDictionary*)attributes 54 | { 55 | return [[ABXFaq alloc] initWithAttributes:attributes]; 56 | } 57 | 58 | - (void)lookForLocalisation:(NSDictionary*)attributes language:(NSString*)language 59 | { 60 | for (NSDictionary *localisation in [attributes objectForKeyNulled:@"localizations"]) { 61 | NSString *languageCode = [localisation valueForKeyPath:@"language.code"]; 62 | if (languageCode && [languageCode caseInsensitiveCompare:language] == NSOrderedSame) { 63 | // Matching localisation 64 | self.question = [localisation objectForKeyNulled:@"question"]; 65 | self.answer = [localisation objectForKeyNulled:@"answer"]; 66 | break; 67 | } 68 | } 69 | } 70 | 71 | + (NSURLSessionDataTask*)fetch:(void(^)(NSArray *faqs, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 72 | { 73 | return [self fetchList:@"faqs" params:nil complete:complete]; 74 | } 75 | 76 | - (NSURLSessionDataTask*)upvote:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 77 | { 78 | return [self vote:@"upvote" complete:complete]; 79 | } 80 | 81 | - (NSURLSessionDataTask*)downvote:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 82 | { 83 | return [self vote:@"downvote" complete:complete]; 84 | } 85 | 86 | - (NSURLSessionDataTask*)vote:(NSString*)action complete:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 87 | { 88 | return [[ABXApiClient instance] PUT:[NSString stringWithFormat:@"faqs/%@/%@", _identifier, action] 89 | params:nil 90 | complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { 91 | if (complete) { 92 | complete(responseCode, httpCode, error); 93 | } 94 | }]; 95 | } 96 | 97 | - (NSURLSessionDataTask*)recordView:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 98 | { 99 | return [[ABXApiClient instance] GET:[NSString stringWithFormat:@"faqs/%@", _identifier] 100 | params:nil 101 | complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { 102 | if (complete) { 103 | complete(responseCode, httpCode, error); 104 | } 105 | }]; 106 | } 107 | 108 | @end 109 | -------------------------------------------------------------------------------- /Classes/Views/ABXNotificationTableViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXNotificationTableViewCell.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 18/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXNotificationTableViewCell.h" 10 | 11 | #import "ABXNotification.h" 12 | 13 | #import "NSString+ABXSizing.h" 14 | 15 | @interface ABXNotificationTableViewCell () 16 | 17 | @property (nonatomic, strong) UILabel *dateLabel; 18 | @property (nonatomic, strong) UILabel *textDetailsLabel; 19 | @property (nonatomic, strong) UIButton *actionButton; 20 | 21 | @end 22 | 23 | @implementation ABXNotificationTableViewCell 24 | 25 | - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 26 | { 27 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 28 | if (self) { 29 | self.layer.shouldRasterize = YES; 30 | self.layer.rasterizationScale = [UIScreen mainScreen].scale; 31 | self.selectionStyle = UITableViewCellSelectionStyleNone; 32 | 33 | // Created date 34 | self.dateLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 10, (CGRectGetWidth(self.contentView.bounds) - 30)/2, 30)]; 35 | self.dateLabel.textColor = [UIColor blackColor]; 36 | self.dateLabel.font = [UIFont systemFontOfSize:15]; 37 | self.dateLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; 38 | [self.contentView addSubview:self.dateLabel]; 39 | 40 | // Text 41 | self.textDetailsLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 40, CGRectGetWidth(self.contentView.bounds) - 30, 0)]; 42 | self.textDetailsLabel.textColor = [UIColor darkGrayColor]; 43 | self.textDetailsLabel.font = [ABXNotificationTableViewCell detailFont]; 44 | self.textDetailsLabel.numberOfLines = 0; 45 | self.textDetailsLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; 46 | [self.contentView addSubview:self.textDetailsLabel]; 47 | 48 | // Action button 49 | self.actionButton = [UIButton buttonWithType:UIButtonTypeSystem]; 50 | if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] == NSOrderedAscending) { 51 | self.actionButton.frame = CGRectMake(20, CGRectGetHeight(self.contentView.bounds) - 38, CGRectGetWidth(self.contentView.bounds) - 40, 32); 52 | } 53 | else { 54 | self.actionButton.frame = CGRectMake(0, CGRectGetHeight(self.contentView.bounds) - 44, CGRectGetWidth(self.contentView.bounds), 44); 55 | } 56 | self.actionButton.autoresizingMask = UIViewAutoresizingFlexibleTopMargin; 57 | [self.actionButton addTarget:self action:@selector(onAction) forControlEvents:UIControlEventTouchUpInside]; 58 | [self.contentView addSubview:self.actionButton]; 59 | } 60 | return self; 61 | } 62 | 63 | - (void)setNotification:(ABXNotification *)notification 64 | { 65 | _notification = notification; 66 | 67 | static dispatch_once_t onceToken; 68 | static NSDateFormatter *dateFormatter = nil; 69 | dispatch_once(&onceToken, ^{ 70 | dateFormatter = [[NSDateFormatter alloc] init]; 71 | [dateFormatter setDateStyle:NSDateFormatterLongStyle]; 72 | }); 73 | 74 | self.dateLabel.text = [dateFormatter stringFromDate:notification.createdAt]; 75 | 76 | self.textDetailsLabel.text = notification.message; 77 | [self setNeedsLayout]; 78 | 79 | if ([notification hasAction]) { 80 | self.actionButton.hidden = NO; 81 | [self.actionButton setTitle:notification.actionLabel forState:UIControlStateNormal]; 82 | } 83 | else { 84 | self.actionButton.hidden = YES; 85 | } 86 | } 87 | 88 | - (void)layoutSubviews 89 | { 90 | [super layoutSubviews]; 91 | 92 | CGRect r = self.textDetailsLabel.frame; 93 | r.size.height = [self.notification.message heightForWidth:CGRectGetWidth(self.contentView.bounds) - 30 andFont:[ABXNotificationTableViewCell detailFont]]; 94 | self.textDetailsLabel.frame = r; 95 | } 96 | 97 | - (void)onAction 98 | { 99 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:self.notification.actionUrl]]; 100 | } 101 | 102 | + (UIFont*)detailFont 103 | { 104 | static dispatch_once_t onceToken; 105 | static UIFont *font = nil; 106 | dispatch_once(&onceToken, ^{ 107 | font = [UIFont systemFontOfSize:14]; 108 | }); 109 | return font; 110 | } 111 | 112 | + (CGFloat)heightForNotification:(ABXNotification*)notification withWidth:(CGFloat)width 113 | { 114 | return [notification.message heightForWidth:width - 30 andFont:[self detailFont]] + 60 + ([notification hasAction] ? 22 : 0); 115 | } 116 | 117 | @end 118 | -------------------------------------------------------------------------------- /Classes/Views/ABXVersionNotificationView.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXVersionNotificationView.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 18/06/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXVersionNotificationView.h" 10 | 11 | #import "ABXAppStore.h" 12 | #import "ABXVersion.h" 13 | #import "ABXVersionsViewController.h" 14 | #import "NSString+ABXLocalized.h" 15 | 16 | @implementation ABXVersionNotificationView 17 | 18 | + (void)fetchAndShowInController:(UIViewController*)controller 19 | foriTunesID:(NSString*)itunesId 20 | backgroundColor:(UIColor*)backgroundColor 21 | textColor:(UIColor*)textColor 22 | buttonColor:(UIColor*)buttonColor 23 | complete:(void(^)(BOOL shown))complete 24 | { 25 | [ABXVersion fetchCurrentVersion:^(ABXVersion *version, ABXVersion *currentVersion, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 26 | if (responseCode == ABXResponseCodeSuccess) { 27 | if (currentVersion && [currentVersion isNewerThanCurrent]) { 28 | // Check if it is live on the store 29 | [currentVersion isLiveVersion:itunesId country:@"us" complete:^(BOOL matches) { 30 | if (matches) { 31 | // Show the view 32 | [ABXNotificationView show:[NSString stringWithFormat:[@"An update to version %@ is available" localizedString], currentVersion.version] 33 | actionText:[@"Update" localizedString] 34 | backgroundColor:backgroundColor 35 | textColor:textColor 36 | buttonColor:buttonColor 37 | inController:controller 38 | actionBlock:^(ABXNotificationView *view) { 39 | // Throw them to the App Store 40 | [view dismiss]; 41 | [ABXAppStore openAppStoreForApp:itunesId]; 42 | } 43 | dismissBlock:^(ABXNotificationView *view) { 44 | // Any action you want 45 | }]; 46 | 47 | if (complete) { 48 | complete(YES); 49 | } 50 | } 51 | else { 52 | if (complete) { 53 | complete(NO); 54 | } 55 | } 56 | }]; 57 | } 58 | else if (version) { 59 | // We got a match! 60 | if (![version hasSeen]) { 61 | // Show the view 62 | [ABXNotificationView show:[NSString stringWithFormat:[@"You've just updated to v%@" localizedString], version.version] 63 | actionText:[@"Learn More" localizedString] 64 | backgroundColor:backgroundColor 65 | textColor:textColor 66 | buttonColor:buttonColor 67 | inController:controller 68 | actionBlock:^(ABXNotificationView *view) { 69 | // Take them to all the versions, or you could choose 70 | // to just show the one version 71 | [version markAsSeen]; 72 | [view dismiss]; 73 | [ABXVersionsViewController showFromController:controller]; 74 | } 75 | dismissBlock:^(ABXNotificationView *view) { 76 | // Here you can mark it as seen if you 77 | // don't want it to appear again 78 | [version markAsSeen]; 79 | }]; 80 | 81 | if (complete) { 82 | complete(YES); 83 | } 84 | } 85 | else { 86 | if (complete) { 87 | complete(NO); 88 | } 89 | } 90 | } 91 | } 92 | else { 93 | if (complete) { 94 | complete(NO); 95 | } 96 | } 97 | }]; 98 | } 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | desc "Runs the specs [EMPTY]" 2 | task :spec do 3 | # Provide your own implementation 4 | end 5 | 6 | task :version do 7 | git_remotes = `git remote`.strip.split("\n") 8 | 9 | if git_remotes.count > 0 10 | puts "-- fetching version number from github" 11 | sh 'git fetch' 12 | 13 | remote_version = remote_spec_version 14 | end 15 | 16 | if remote_version.nil? 17 | puts "There is no current released version. You're about to release a new Pod." 18 | version = "0.0.1" 19 | else 20 | puts "The current released version of your pod is " + 21 | remote_spec_version.to_s() 22 | version = suggested_version_number 23 | end 24 | 25 | puts "Enter the version you want to release (" + version + ") " 26 | new_version_number = $stdin.gets.strip 27 | if new_version_number == "" 28 | new_version_number = version 29 | end 30 | 31 | replace_version_number(new_version_number) 32 | end 33 | 34 | desc "Release a new version of the Pod (append repo=name to push to a private spec repo)" 35 | task :release do 36 | # Allow override of spec repo name using `repo=private` after task name 37 | repo = ENV["repo"] || "master" 38 | 39 | puts "* Running version" 40 | sh "rake version" 41 | 42 | unless ENV['SKIP_CHECKS'] 43 | if `git symbolic-ref HEAD 2>/dev/null`.strip.split('/').last != 'master' 44 | $stderr.puts "[!] You need to be on the `master' branch in order to be able to do a release." 45 | exit 1 46 | end 47 | 48 | if `git tag`.strip.split("\n").include?(spec_version) 49 | $stderr.puts "[!] A tag for version `#{spec_version}' already exists. Change the version in the podspec" 50 | exit 1 51 | end 52 | 53 | puts "You are about to release `#{spec_version}`, is that correct? [y/n]" 54 | exit if $stdin.gets.strip.downcase != 'y' 55 | end 56 | 57 | puts "* Running specs" 58 | sh "rake spec" 59 | 60 | puts "* Linting the podspec" 61 | sh "pod lib lint" 62 | 63 | # Then release 64 | sh "git commit #{podspec_path} CHANGELOG.md -m 'Release #{spec_version}' --allow-empty" 65 | sh "git tag -a #{spec_version} -m 'Release #{spec_version}'" 66 | sh "git push origin master" 67 | sh "git push origin --tags" 68 | if repo == "master" 69 | sh "pod trunk push #{podspec_path}" 70 | else 71 | sh "pod repo push #{repo} #{podspec_path}" 72 | end 73 | end 74 | 75 | # @return [Pod::Version] The version as reported by the Podspec. 76 | # 77 | def spec_version 78 | require 'cocoapods' 79 | spec = Pod::Specification.from_file(podspec_path) 80 | spec.version 81 | end 82 | 83 | # @return [Pod::Version] The version as reported by the Podspec from remote. 84 | # 85 | def remote_spec_version 86 | require 'cocoapods-core' 87 | 88 | if spec_file_exist_on_remote? 89 | remote_spec = eval(`git show origin/master:#{podspec_path}`) 90 | remote_spec.version 91 | else 92 | nil 93 | end 94 | end 95 | 96 | # @return [Bool] If the remote repository has a copy of the podpesc file or not. 97 | # 98 | def spec_file_exist_on_remote? 99 | test_condition = `if git rev-parse --verify --quiet origin/master:#{podspec_path} >/dev/null; 100 | then 101 | echo 'true' 102 | else 103 | echo 'false' 104 | fi` 105 | 106 | 'true' == test_condition.strip 107 | end 108 | 109 | # @return [String] The relative path of the Podspec. 110 | # 111 | def podspec_path 112 | podspecs = Dir.glob('*.podspec') 113 | if podspecs.count == 1 114 | podspecs.first 115 | else 116 | raise "Could not select a podspec" 117 | end 118 | end 119 | 120 | # @return [String] The suggested version number based on the local and remote 121 | # version numbers. 122 | # 123 | def suggested_version_number 124 | if spec_version != remote_spec_version 125 | spec_version.to_s() 126 | else 127 | next_version(spec_version).to_s() 128 | end 129 | end 130 | 131 | # @param [Pod::Version] version 132 | # the version for which you need the next version 133 | # 134 | # @note It is computed by bumping the last component of 135 | # the version string by 1. 136 | # 137 | # @return [Pod::Version] The version that comes next after 138 | # the version supplied. 139 | # 140 | def next_version(version) 141 | version_components = version.to_s().split("."); 142 | last = (version_components.last.to_i() + 1).to_s 143 | version_components[-1] = last 144 | Pod::Version.new(version_components.join(".")) 145 | end 146 | 147 | # @param [String] new_version_number 148 | # the new version number 149 | # 150 | # @note This methods replaces the version number in the podspec file 151 | # with a new version number. 152 | # 153 | # @return void 154 | # 155 | def replace_version_number(new_version_number) 156 | text = File.read(podspec_path) 157 | text.gsub!(/(s.version( )*= ")#{spec_version}(")/, 158 | "\\1#{new_version_number}\\3") 159 | File.open(podspec_path, "w") { |file| file.puts text } 160 | end 161 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXBaseListViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXBaseListViewController.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 22/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXBaseListViewController.h" 10 | 11 | #import "ABXNavigationController.h" 12 | 13 | #import "NSString+ABXLocalized.h" 14 | 15 | @interface ABXBaseListViewController() 16 | 17 | @end 18 | 19 | @implementation ABXBaseListViewController 20 | 21 | - (void)dealloc 22 | { 23 | self.tableView.delegate = nil; 24 | self.tableView.dataSource= nil; 25 | } 26 | 27 | - (void)viewDidLoad 28 | { 29 | [super viewDidLoad]; 30 | 31 | self.view.backgroundColor = [UIColor whiteColor]; 32 | 33 | [self setupUI]; 34 | } 35 | 36 | - (void)didReceiveMemoryWarning 37 | { 38 | [super didReceiveMemoryWarning]; 39 | // Dispose of any resources that can be recreated. 40 | } 41 | 42 | + (void)showFromController:(UIViewController*)controller 43 | { 44 | ABXBaseListViewController *viewController = [[self alloc] init]; 45 | UINavigationController *nav = [[ABXNavigationController alloc] initWithRootViewController:viewController]; 46 | if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { 47 | // Show as a sheet on iPad 48 | nav.modalPresentationStyle = UIModalPresentationFormSheet; 49 | } 50 | [controller presentViewController:nav animated:YES completion:nil]; 51 | } 52 | 53 | #pragma mark - UI 54 | 55 | - (void)setupUI 56 | { 57 | // Table view 58 | self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds]; 59 | self.tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; 60 | self.tableView.dataSource = (id)self; 61 | self.tableView.delegate = (id)self; 62 | self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectZero]; 63 | self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 44)]; 64 | self.tableView.tableFooterView.backgroundColor = [UIColor clearColor]; 65 | [self.view addSubview:self.tableView]; 66 | 67 | // Powered by 68 | UIButton *appbotButton = [UIButton buttonWithType:UIButtonTypeCustom]; 69 | appbotButton.frame = CGRectMake(0, CGRectGetHeight(self.view.frame) - 33, CGRectGetWidth(self.view.frame), 33); 70 | appbotButton.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1]; 71 | [appbotButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; 72 | [appbotButton setTitle:[[@"Powered by" localizedString] stringByAppendingString:@" Appbot"] forState:UIControlStateNormal]; 73 | appbotButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; 74 | appbotButton.titleLabel.font = [UIFont systemFontOfSize:13]; 75 | [appbotButton addTarget:self action:@selector(onAppbot) forControlEvents:UIControlEventTouchUpInside]; 76 | [self.view addSubview:appbotButton]; 77 | 78 | // Powered by seperator 79 | UIView *seperator = [[UIView alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.frame) - 33, CGRectGetWidth(self.view.frame), 1)]; 80 | seperator.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; 81 | seperator.backgroundColor = [UIColor colorWithWhite:0.8 alpha:1]; 82 | [self.view addSubview:seperator]; 83 | 84 | // Activity Indicator 85 | self.activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; 86 | self.activityView.center = CGPointMake(CGRectGetMidX(self.view.bounds), 100); 87 | [self.activityView startAnimating]; 88 | self.activityView.hidesWhenStopped = YES; 89 | self.activityView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; 90 | [self.view addSubview:self.activityView]; 91 | 92 | // Only show the close button if we are at the root controller 93 | if (self.navigationController.viewControllers.count == 1) { 94 | self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] 95 | initWithBarButtonSystemItem:UIBarButtonSystemItemDone 96 | target:self 97 | action:@selector(onDone)]; 98 | } 99 | } 100 | 101 | #pragma mark - Buttons 102 | 103 | - (void)onDone 104 | { 105 | [self.navigationController dismissViewControllerAnimated:YES completion:nil]; 106 | } 107 | 108 | - (void)onAppbot 109 | { 110 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://appbot.co"]]; 111 | } 112 | 113 | #pragma mark - Errors 114 | 115 | - (void)showError:(NSString*)error 116 | { 117 | if (!self.errorLabel) { 118 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10, 150, CGRectGetWidth(self.tableView.bounds) - 20, 100)]; 119 | label.textAlignment = NSTextAlignmentCenter; 120 | label.numberOfLines = 0; 121 | label.text = error; 122 | label.font = [UIFont systemFontOfSize:15]; 123 | label.textColor = [UIColor blackColor]; 124 | label.backgroundColor = [UIColor clearColor]; 125 | label.autoresizingMask = UIViewAutoresizingFlexibleWidth; 126 | [self.view addSubview:label]; 127 | self.errorLabel = label; 128 | } 129 | else { 130 | self.errorLabel.text = error; 131 | self.errorLabel.hidden = NO; 132 | } 133 | } 134 | 135 | @end 136 | -------------------------------------------------------------------------------- /Classes/Models/ABXIssue.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXIssue.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXIssue.h" 9 | 10 | #include 11 | #include 12 | #import 13 | #import 14 | 15 | @implementation ABXIssue 16 | 17 | + (NSURLSessionDataTask*)submit:(NSString*)email 18 | feedback:(NSString*)feedback 19 | attachments:(NSArray*)attachments 20 | metaData:(NSDictionary*)metaData 21 | complete:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 22 | { 23 | NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:[self systemInfo]]; 24 | if (feedback.length > 0) { 25 | [params setObject:feedback forKey:@"issue"]; 26 | } 27 | if (email.length > 0) { 28 | [params setObject:email forKey:@"email"]; 29 | } 30 | if (metaData) { 31 | [params setObject:metaData forKey:@"meta_data"]; 32 | } 33 | if (attachments.count > 0) { 34 | [params setObject:[attachments valueForKeyPath:@"identifier"] forKey:@"attachment_ids"]; 35 | } 36 | 37 | return [[ABXApiClient instance] POST:@"issues" 38 | params:params 39 | complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { 40 | if (complete) { 41 | complete(responseCode, httpCode, error); 42 | } 43 | }]; 44 | } 45 | 46 | #pragma mark - System Info 47 | 48 | + (NSDictionary*)systemInfo 49 | { 50 | NSUInteger totalMemory; 51 | NSUInteger freeMemory = [self freeMemory:&totalMemory]; 52 | 53 | uint64_t totalSpace; 54 | uint64_t freeSpace = [self freeDiskspace:&totalSpace]; 55 | 56 | return @{ @"os_version" : [@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion] ?: @""], 57 | @"app_version" : NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"] ?: @"", 58 | @"app_version_short" : NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"] ?: @"", 59 | @"app_name" : NSBundle.mainBundle.infoDictionary[@"CFBundleDisplayName"] ?: @"", 60 | @"device" : [self platform] ?: @"", 61 | @"language" : [[NSLocale preferredLanguages] firstObject] ?: @"", 62 | @"locale" : [[NSLocale currentLocale] localeIdentifier] ?: @"", 63 | @"jailbroken" : @([self isJailbroken]), 64 | @"free_memory" : @(freeMemory), 65 | @"total_memory" : @(totalMemory), 66 | @"free_space" : @(freeSpace), 67 | @"total_space" : @(totalSpace) }; 68 | } 69 | 70 | + (NSString *)platform 71 | { 72 | size_t size; 73 | sysctlbyname("hw.machine", NULL, &size, NULL, 0); 74 | char *machine = malloc(size); 75 | sysctlbyname("hw.machine", machine, &size, NULL, 0); 76 | NSString *platform = [NSString stringWithUTF8String:machine]; 77 | free(machine); 78 | return platform; 79 | } 80 | 81 | + (NSUInteger)freeMemory:(NSUInteger*)totalMemory 82 | { 83 | mach_port_t host_port = mach_host_self(); 84 | mach_msg_type_number_t host_size = sizeof(vm_statistics_data_t) / sizeof(integer_t); 85 | vm_size_t pagesize; 86 | vm_statistics_data_t vm_stat; 87 | 88 | host_page_size(host_port, &pagesize); 89 | 90 | host_statistics(host_port, HOST_VM_INFO, (host_info_t)&vm_stat, &host_size); 91 | 92 | NSUInteger mem_used = (vm_stat.active_count + vm_stat.inactive_count + vm_stat.wire_count) * pagesize; 93 | NSUInteger mem_free = vm_stat.free_count * pagesize; 94 | NSUInteger mem_total = mem_used + mem_free; 95 | 96 | *totalMemory = mem_total; 97 | 98 | return mem_free; 99 | } 100 | 101 | + (uint64_t)freeDiskspace:(uint64_t*)totalDiskspace 102 | { 103 | uint64_t totalSpace = 0; 104 | uint64_t totalFreeSpace = 0; 105 | NSError *error = nil; 106 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 107 | NSDictionary *dictionary = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject] error: &error]; 108 | 109 | if (dictionary) { 110 | NSNumber *fileSystemSizeInBytes = [dictionary objectForKey: NSFileSystemSize]; 111 | NSNumber *freeFileSystemSizeInBytes = [dictionary objectForKey:NSFileSystemFreeSize]; 112 | totalSpace = [fileSystemSizeInBytes unsignedLongLongValue]; 113 | totalFreeSpace = [freeFileSystemSizeInBytes unsignedLongLongValue]; 114 | } 115 | 116 | *totalDiskspace = totalSpace; 117 | 118 | return totalFreeSpace; 119 | } 120 | 121 | // All credit to https://github.com/itruf/crackify 122 | + (BOOL)isJailbroken 123 | { 124 | #if !TARGET_IPHONE_SIMULATOR 125 | //Check for Cydia.app 126 | BOOL yes; 127 | if ([[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"/%@%@%@%@%@%@%@", @"App", @"lic",@"ati", @"ons/", @"Cyd", @"ia.", @"app"]] 128 | || [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"/%@%@%@%@%@%@", @"pr", @"iva",@"te/v", @"ar/l", @"ib/a", @"pt/"] isDirectory:&yes] 129 | || [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"/%@%@%@%@%@%@", @"us", @"r/l",@"ibe", @"xe", @"c/cy", @"dia"] isDirectory:&yes]) { 130 | //Cydia installed 131 | return YES; 132 | } 133 | 134 | //Try to write file in private 135 | NSError *error; 136 | 137 | static NSString *str = @"Jailbreak test string"; 138 | 139 | [str writeToFile:@"/private/test_jail.txt" atomically:YES 140 | encoding:NSUTF8StringEncoding error:&error]; 141 | 142 | if (error == nil) { 143 | // Writed 144 | return YES; 145 | } 146 | else { 147 | [[NSFileManager defaultManager] removeItemAtPath:@"/private/test_jail.txt" error:nil]; 148 | } 149 | #endif 150 | return NO; 151 | } 152 | 153 | 154 | @end 155 | -------------------------------------------------------------------------------- /Example/Sample Project/ABXViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXViewController.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 21/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXViewController.h" 10 | 11 | #import "ABX.h" 12 | #import "ABXPromptView.h" 13 | #import "NSString+ABXLocalized.h" 14 | #import "UIViewController+ABXScreenshot.h" 15 | 16 | @interface ABXViewController () 17 | 18 | @property (nonatomic, strong) ABXPromptView *promptView; 19 | 20 | @property (nonatomic, strong) IBOutlet UIScrollView *scrollView; 21 | 22 | @end 23 | 24 | @implementation ABXViewController 25 | 26 | static NSString* const kiTunesID = @"650762525"; 27 | 28 | - (void)viewDidLoad 29 | { 30 | [super viewDidLoad]; 31 | 32 | // The prompt view is an example workflow using AppbotX 33 | // It's also good to only show it after a positive interaction 34 | // or a number of usages of the app 35 | if (![ABXPromptView hasHadInteractionForCurrentVersion]) { 36 | self.promptView = [[ABXPromptView alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.bounds) - 100, CGRectGetWidth(self.view.bounds), 100)]; 37 | self.promptView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth; 38 | [self.view addSubview:self.promptView]; 39 | self.promptView.delegate = self; 40 | } 41 | } 42 | 43 | - (void)didReceiveMemoryWarning 44 | { 45 | [super didReceiveMemoryWarning]; 46 | // Dispose of any resources that can be recreated. 47 | } 48 | 49 | - (void)viewDidLayoutSubviews 50 | { 51 | [_scrollView setContentSize:CGSizeMake(CGRectGetWidth(self.view.frame), 460)]; 52 | } 53 | 54 | - (BOOL)prefersStatusBarHidden 55 | { 56 | return NO; 57 | } 58 | 59 | #pragma mark - Buttons 60 | 61 | - (IBAction)onFetchNotifications:(id)sender 62 | { 63 | [ABXNotificationView fetchAndShowInController:self 64 | backgroundColor:[UIColor colorWithRed:0x86/255.0 green:0xcc/255.0 blue:0xf1/255.0 alpha:1] 65 | textColor:[UIColor blackColor] 66 | buttonColor:[UIColor whiteColor] 67 | complete:^(BOOL shown) { 68 | // Here you may want to chain fetching versions 69 | // if it wasn't shown 70 | }]; 71 | } 72 | 73 | - (IBAction)onFetchVersions:(id)sender 74 | { 75 | [ABXVersion fetch:^(NSArray *versions, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 76 | switch (responseCode) { 77 | case ABXResponseCodeSuccess: { 78 | [self showAlert:@"Versions" message:[NSString stringWithFormat:@"Received %ld versions", (unsigned long)versions.count]]; 79 | } 80 | break; 81 | 82 | default: { 83 | [self showAlert:@"Versions" message:[NSString stringWithFormat:@"%u", responseCode]]; 84 | } 85 | break; 86 | } 87 | }]; 88 | } 89 | 90 | - (IBAction)onFetchCurrentVersion:(id)sender 91 | { 92 | // This is a convenient wrapper, or dig in and control it yourself 93 | [ABXVersionNotificationView fetchAndShowInController:self 94 | foriTunesID:kiTunesID 95 | backgroundColor:[UIColor colorWithRed:0xf4/255.0 green:0x7d/255.0 blue:0x67/255.0 alpha:1] 96 | textColor:[UIColor blackColor] 97 | buttonColor:[UIColor whiteColor] 98 | complete:^(BOOL shown) { 99 | // Here you may want to chain fetching notifications 100 | // if it wasn't shown 101 | }]; 102 | } 103 | 104 | - (IBAction)onFetchFAQs:(id)sender 105 | { 106 | [ABXFaq fetch:^(NSArray *faqs, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 107 | switch (responseCode) { 108 | case ABXResponseCodeSuccess: { 109 | [self showAlert:@"FAQs" message:[NSString stringWithFormat:@"Received %ld faqs", (unsigned long)faqs.count]]; 110 | } 111 | break; 112 | 113 | default: { 114 | [self showAlert:@"FAQs" message:[NSString stringWithFormat:@"%u", responseCode]]; 115 | } 116 | break; 117 | } 118 | }]; 119 | } 120 | 121 | - (IBAction)showFAQs:(id)sender 122 | { 123 | [ABXFAQsViewController showFromController:self hideContactButton:NO contactMetaData:nil initialSearch:nil]; 124 | } 125 | 126 | - (IBAction)showVersions:(id)sender 127 | { 128 | [ABXVersionsViewController showFromController:self]; 129 | } 130 | 131 | - (IBAction)showNotifications:(id)sender 132 | { 133 | [ABXNotificationsViewController showFromController:self]; 134 | } 135 | 136 | - (IBAction)showFeedback:(id)sender 137 | { 138 | [ABXFeedbackViewController showFromController:self placeholder:nil email:nil metaData:nil image:nil delegate:nil]; 139 | } 140 | 141 | - (IBAction)showFeedbackWithImage:(id)sender 142 | { 143 | // An example of the feedback window that you might launch from a 'report an issue' button 144 | // Where some meta data and a screenshot is attached 145 | [ABXFeedbackViewController showFromController:self placeholder:nil email:nil metaData:@{ @"BugPrompt" : @YES } image:[self takeScreenshot] delegate:nil]; 146 | } 147 | 148 | #pragma mark - Alert 149 | 150 | - (void)showAlert:(NSString*)title message:(NSString*)message 151 | { 152 | [[[UIAlertView alloc] initWithTitle:title 153 | message:message 154 | delegate:nil 155 | cancelButtonTitle:[@"OK" localizedString] 156 | otherButtonTitles:nil] show]; 157 | } 158 | 159 | #pragma mark - ABXPromptViewDelegate 160 | 161 | - (void)appbotPromptForReview 162 | { 163 | [ABXAppStore openAppStoreReviewForApp:kiTunesID]; 164 | self.promptView.hidden = YES; 165 | } 166 | 167 | - (void)appbotPromptForFeedback 168 | { 169 | [ABXFeedbackViewController showFromController:self placeholder:nil delegate:nil]; 170 | self.promptView.hidden = YES; 171 | } 172 | 173 | - (void)appbotPromptClose 174 | { 175 | self.promptView.hidden = YES; 176 | } 177 | 178 | - (void)appbotPromptLiked 179 | { 180 | 181 | } 182 | 183 | - (void)appbotPromptDidntLike 184 | { 185 | 186 | } 187 | 188 | @end 189 | -------------------------------------------------------------------------------- /Classes/Views/ABXPromptView.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXPromptView.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 30/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXPromptView.h" 10 | 11 | #import "NSString+ABXLocalized.h" 12 | 13 | @interface ABXPromptView () 14 | 15 | @property (nonatomic, strong) UIView *container; 16 | @property (nonatomic, strong) UIButton *closeButton; 17 | 18 | @property (nonatomic, assign) BOOL step2; 19 | @property (nonatomic, assign) BOOL liked; 20 | 21 | @end 22 | 23 | @implementation ABXPromptView 24 | 25 | - (id)initWithFrame:(CGRect)frame 26 | { 27 | self = [super initWithFrame:frame]; 28 | if (self) { 29 | [self initialise]; 30 | } 31 | return self; 32 | } 33 | 34 | - (id)initWithCoder:(NSCoder *)aDecoder 35 | { 36 | self = [super initWithCoder:aDecoder]; 37 | if (self) { 38 | [self initialise]; 39 | } 40 | return self; 41 | } 42 | 43 | #pragma mark - Setup 44 | 45 | - (void)initialise 46 | { 47 | self.container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 280, 100)]; 48 | self.container.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; 49 | self.container.backgroundColor = [UIColor clearColor]; 50 | [self addSubview:self.container]; 51 | self.container.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); 52 | 53 | self.label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.container.bounds), 52)]; 54 | self.label.textColor = [UIColor colorWithRed:50/255.0f green:65/255.0f blue:85/255.0f alpha:1.0f]; 55 | self.label.textAlignment = NSTextAlignmentCenter; 56 | self.label.numberOfLines = 0; 57 | self.label.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:15]; 58 | 59 | self.label.text = NSLocalizedString(@"What do you think about WordPress?", @"This is the string we display when prompting the user to review the app"); 60 | [self.container addSubview:self.label]; 61 | 62 | self.leftButton = [UIButton buttonWithType:UIButtonTypeCustom]; 63 | self.leftButton.frame = CGRectMake(CGRectGetMidX(self.container.bounds) - 135, 50, 130, 30); 64 | self.leftButton.backgroundColor = [UIColor colorWithRed:0/255.0f green:170/255.0f blue:220/255.0f alpha:1.0f]; 65 | 66 | self.leftButton.layer.cornerRadius = 4; 67 | self.leftButton.layer.masksToBounds = YES; 68 | [self.leftButton setTitle:NSLocalizedString(@"I Like It", @"This is one of the buttons we display inside of the prompt to review the app") forState:UIControlStateNormal]; 69 | [self.leftButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 70 | self.leftButton.titleLabel.font = [UIFont systemFontOfSize:15]; 71 | [self.leftButton addTarget:self action:@selector(onLove) forControlEvents:UIControlEventTouchUpInside]; 72 | [self.container addSubview:self.leftButton]; 73 | 74 | self.rightButton = [UIButton buttonWithType:UIButtonTypeCustom]; 75 | self.rightButton.frame = CGRectMake(CGRectGetMidX(self.container.bounds) + 5, 50, 130, 30); 76 | self.rightButton.backgroundColor = [UIColor colorWithRed:144/255.0f green:174/255.0f blue:194/255.0f alpha:1.0f]; 77 | self.rightButton.layer.cornerRadius = 4; 78 | self.rightButton.layer.masksToBounds = YES; 79 | [self.rightButton setTitle:NSLocalizedString(@"Could Be Better", @"This is one of the buttons we display inside of the prompt to review the app") forState:UIControlStateNormal]; 80 | [self.rightButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 81 | self.rightButton.titleLabel.font = [UIFont systemFontOfSize:15]; 82 | self.rightButton.titleLabel.textColor = [UIColor colorWithRed:50/255.0f green:65/255.0f blue:85/255.0f alpha:1.0f]; 83 | [self.rightButton addTarget:self action:@selector(onImprove) forControlEvents:UIControlEventTouchUpInside]; 84 | [self.container addSubview:self.rightButton]; 85 | } 86 | 87 | #pragma mark - Buttons 88 | 89 | - (void)onLove 90 | { 91 | if (self.step2) { 92 | [[self class] setHasHadInteractionForCurrentVersion]; 93 | if (self.liked && self.delegate && [self.delegate respondsToSelector:@selector(appbotPromptForReview)]) { 94 | [self.delegate appbotPromptForReview]; 95 | } 96 | else if (!self.liked && self.delegate && [self.delegate respondsToSelector:@selector(appbotPromptForFeedback)]) { 97 | [self.delegate appbotPromptForFeedback]; 98 | } 99 | } 100 | else { 101 | [self.delegate appbotPromptLiked]; 102 | self.liked = YES; 103 | self.step2 = YES; 104 | [UIView animateWithDuration:0.3 105 | animations:^{ 106 | self.label.text = NSLocalizedString(@"Great! Could you leave us a nice review?\nIt really helps.", @"This is the text we display to the user when we ask them for a review and they've indicated they like the app"); 107 | [self.leftButton setTitle:NSLocalizedString(@"Leave a Review", @"This is one of the buttons we display when prompting the user for a review")forState:UIControlStateNormal]; 108 | [self.rightButton setTitle:NSLocalizedString(@"No Thanks", @"This is one of the buttons we display when prompting the user for a review") forState:UIControlStateNormal]; 109 | }]; 110 | } 111 | } 112 | 113 | - (void)onImprove 114 | { 115 | if (self.step2) { 116 | [[self class] setHasHadInteractionForCurrentVersion]; 117 | if (self.delegate && [self.delegate respondsToSelector:@selector(appbotPromptClose)]) { 118 | [self.delegate appbotPromptClose]; 119 | } 120 | } 121 | else { 122 | [self.delegate appbotPromptDidntLike]; 123 | self.liked = NO; 124 | self.step2 = YES; 125 | [UIView animateWithDuration:0.3 126 | animations:^{ 127 | self.label.text = NSLocalizedString(@"Could you tell us how we could improve?", @"This is the text we display to the user when we ask them for a review and they've indicated they don't like the app"); 128 | [self.leftButton setTitle:NSLocalizedString(@"Send Feedback", @"This is one of the buttons we display when prompting the user for a review") forState:UIControlStateNormal]; 129 | [self.rightButton setTitle:NSLocalizedString(@"No Thanks", @"This is one of the buttons we display when prompting the user for a review") forState:UIControlStateNormal]; 130 | }]; 131 | } 132 | } 133 | 134 | static NSString* const kInteractionKey = @"ABXPromptViewInteraction"; 135 | 136 | + (NSString*)keyForCurrentVersion 137 | { 138 | NSString *version = NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"] ?: NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]; 139 | return [kInteractionKey stringByAppendingString:version]; 140 | } 141 | 142 | + (BOOL)hasHadInteractionForCurrentVersion 143 | { 144 | return [[NSUserDefaults standardUserDefaults] boolForKey:[self keyForCurrentVersion]]; 145 | } 146 | 147 | + (void)setHasHadInteractionForCurrentVersion 148 | { 149 | [[NSUserDefaults standardUserDefaults] setBool:YES forKey:[self keyForCurrentVersion]]; 150 | [[NSUserDefaults standardUserDefaults] synchronize]; 151 | } 152 | 153 | @end 154 | -------------------------------------------------------------------------------- /Classes/Views/ABXNotificationView.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXNotificationView.m 3 | // Sample Project 4 | // 5 | // Created by Stuart Hall on 30/05/2014. 6 | // Copyright (c) 2014 Appbot. All rights reserved. 7 | // 8 | 9 | #import "ABXNotificationView.h" 10 | 11 | #import "ABXNotification.h" 12 | 13 | #import "NSString+ABXSizing.h" 14 | #import "NSString+ABXLocalized.h" 15 | 16 | @interface ABXNotificationView () 17 | 18 | @property (nonatomic, strong) ABXNotificationViewCallback actionCallback; 19 | @property (nonatomic, strong) ABXNotificationViewCallback dismissCallback; 20 | 21 | @end 22 | 23 | @implementation ABXNotificationView 24 | 25 | + (ABXNotificationView*)show:(NSString*)text 26 | actionText:(NSString*)actionText 27 | backgroundColor:(UIColor*)backgroundColor 28 | textColor:(UIColor*)textColor 29 | buttonColor:(UIColor*)buttonColor 30 | inController:(UIViewController*)controller 31 | actionBlock:(ABXNotificationViewCallback)actionBlock 32 | dismissBlock:(ABXNotificationViewCallback)dismissBlock 33 | { 34 | static NSInteger const kMaxWidth = 300; 35 | 36 | 37 | // Calculate the label height 38 | UIFont *font = [UIFont systemFontOfSize:15]; 39 | CGFloat labelHeight = [text heightForWidth:kMaxWidth andFont:font]; 40 | 41 | NSUInteger topPadding = [self topOffsetForController:controller]; 42 | 43 | // Create the view 44 | CGFloat totalHeight = labelHeight + 50 + topPadding; 45 | ABXNotificationView *view = [[ABXNotificationView alloc] initWithFrame:CGRectMake(0, -totalHeight, CGRectGetWidth(controller.view.bounds), totalHeight)]; 46 | view.backgroundColor = backgroundColor; 47 | view.autoresizingMask = UIViewAutoresizingFlexibleWidth; 48 | view.actionCallback = actionBlock; 49 | view.dismissCallback = dismissBlock; 50 | [controller.view addSubview:view]; 51 | 52 | // Label for the text 53 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake((CGRectGetWidth(view.bounds) - kMaxWidth)/2, 15 + topPadding, kMaxWidth, labelHeight)]; 54 | label.numberOfLines = 0; 55 | label.text = text; 56 | label.textAlignment = NSTextAlignmentCenter; 57 | label.textColor = textColor; 58 | label.font = font; 59 | label.backgroundColor = [UIColor clearColor]; 60 | label.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; 61 | [view addSubview:label]; 62 | 63 | if (actionText && actionText.length > 0) { 64 | // Action button 65 | UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; 66 | button.tintColor = buttonColor; 67 | [button setTitle:actionText forState:UIControlStateNormal]; 68 | button.frame = CGRectMake((CGRectGetWidth(view.bounds) - kMaxWidth)/2, totalHeight - 40, kMaxWidth/2, 40); 69 | button.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; 70 | button.titleLabel.font = [UIFont boldSystemFontOfSize:15]; 71 | [button addTarget:view action:@selector(onAction:) forControlEvents:UIControlEventTouchUpInside]; 72 | [view addSubview:button]; 73 | } 74 | 75 | // Close Button 76 | UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; 77 | button.tintColor = buttonColor; 78 | [button setTitle:[@"Close" localizedString] forState:UIControlStateNormal]; 79 | if (actionText && actionText.length > 0) { 80 | button.frame = CGRectMake(CGRectGetWidth(view.bounds) / 2, totalHeight - 40, kMaxWidth / 2, 40); 81 | } 82 | else { 83 | button.frame = CGRectMake(0, totalHeight - 40, CGRectGetWidth(view.bounds), 40); 84 | } 85 | button.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; 86 | button.titleLabel.font = [UIFont boldSystemFontOfSize:15]; 87 | [button addTarget:view action:@selector(onClose:) forControlEvents:UIControlEventTouchUpInside]; 88 | [view addSubview:button]; 89 | 90 | // Slide it down 91 | [UIView animateWithDuration:0.3 92 | animations:^{ 93 | CGFloat topPadding = 0; 94 | CGRect r = view.frame; 95 | r.origin.y = topPadding; 96 | view.frame = r; 97 | }]; 98 | 99 | return view; 100 | } 101 | 102 | - (void)onAction:(UIButton*)button 103 | { 104 | if (self.actionCallback) { 105 | self.actionCallback(self); 106 | } 107 | } 108 | 109 | - (void)onClose:(UIButton*)button 110 | { 111 | if (self.dismissCallback) { 112 | self.dismissCallback(self); 113 | } 114 | [self dismiss]; 115 | } 116 | 117 | - (void)dismiss 118 | { 119 | // Slide it away 120 | [UIView animateWithDuration:0.3 121 | animations:^{ 122 | CGRect r = self.frame; 123 | r.origin.y = -CGRectGetHeight(r); 124 | self.frame = r; 125 | } 126 | completion:^(BOOL finished) { 127 | [self removeFromSuperview]; 128 | }]; 129 | } 130 | 131 | + (NSInteger)topOffsetForController:(UIViewController*)controller 132 | { 133 | if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] != NSOrderedAscending) { 134 | // Determine the status bar size 135 | CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; 136 | CGRect statusBarWindowRect = [controller.view.window convertRect:statusBarFrame fromWindow: nil]; 137 | CGRect statusBarViewRect = [controller.view convertRect:statusBarWindowRect fromView: nil]; 138 | 139 | // Determine the navigation bar size 140 | CGFloat navbarHeight = CGRectGetHeight(controller.navigationController.navigationBar.frame); 141 | 142 | return CGRectGetHeight(statusBarViewRect) + navbarHeight; 143 | } 144 | return 0; 145 | } 146 | 147 | + (void)fetchAndShowInController:(UIViewController*)controller 148 | backgroundColor:(UIColor*)backgroundColor 149 | textColor:(UIColor*)textColor 150 | buttonColor:(UIColor*)buttonColor 151 | complete:(void(^)(BOOL shown))complete 152 | { 153 | // Fetch the notifications, there will only ever be one 154 | [ABXNotification fetchActive:^(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 155 | if (responseCode == ABXResponseCodeSuccess) { 156 | if (notifications.count > 0) { 157 | ABXNotification *notification = [notifications firstObject]; 158 | 159 | if (![notification hasSeen]) { 160 | // Show the view 161 | [ABXNotificationView show:notification.message 162 | actionText:notification.actionLabel 163 | backgroundColor:backgroundColor 164 | textColor:textColor 165 | buttonColor:buttonColor 166 | inController:controller 167 | actionBlock:^(ABXNotificationView *view) { 168 | // Open the URL 169 | // Here you could open it in your internal UIWebView or route accordingly 170 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:notification.actionUrl]]; 171 | [notification markAsSeen]; 172 | } dismissBlock:^(ABXNotificationView *view) { 173 | // Here you can mark it as seen if you 174 | // don't want it to appear again 175 | [notification markAsSeen]; 176 | }]; 177 | 178 | if (complete) { 179 | complete(YES); 180 | } 181 | 182 | return; 183 | } 184 | } 185 | } 186 | 187 | 188 | if (complete) { 189 | complete(NO); 190 | } 191 | }]; 192 | } 193 | 194 | @end 195 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXFAQsViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXFAQsViewController.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXFAQsViewController.h" 9 | 10 | #import "ABXFaq.h" 11 | #import "ABXFAQViewController.h" 12 | #import "ABXFeedbackViewController.h" 13 | #import "ABXFAQTableViewCell.h" 14 | #import "ABXNavigationController.h" 15 | #import "NSString+ABXLocalized.h" 16 | 17 | @interface ABXFAQsViewController () 18 | 19 | @property (nonatomic, strong) NSArray *faqs; 20 | @property (nonatomic, strong) NSArray *filteredFaqs; 21 | 22 | @property (nonatomic, strong) UISearchBar *searchBar; 23 | 24 | @end 25 | 26 | @implementation ABXFAQsViewController 27 | 28 | + (void)showFromController:(UIViewController*)controller hideContactButton:(BOOL)hideContactButton contactMetaData:(NSDictionary*)contactMetaData initialSearch:(NSString*)initialSearch 29 | { 30 | ABXFAQsViewController *viewController = [[self alloc] init]; 31 | viewController.hideContactButton = hideContactButton; 32 | viewController.contactMetaData = contactMetaData; 33 | viewController.initialSearch = initialSearch; 34 | UINavigationController *nav = [[ABXNavigationController alloc] initWithRootViewController:viewController]; 35 | if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { 36 | // Show as a sheet on iPad 37 | nav.modalPresentationStyle = UIModalPresentationFormSheet; 38 | } 39 | [controller presentViewController:nav animated:YES completion:nil]; 40 | } 41 | 42 | - (void)viewDidLoad 43 | { 44 | [super viewDidLoad]; 45 | 46 | // Title 47 | self.title = [@"FAQs" localizedString]; 48 | 49 | // Setup our UI components 50 | [self setupFaqUI]; 51 | 52 | // Fetch 53 | if (![ABXApiClient isInternetReachable]) { 54 | [self.activityView stopAnimating]; 55 | [self showError:[@"No Internet" localizedString]]; 56 | } 57 | else { 58 | [self fetchFAQs]; 59 | } 60 | } 61 | 62 | - (void)viewWillAppear:(BOOL)animated 63 | { 64 | [super viewWillAppear:animated]; 65 | 66 | // Show the keyboard again if it was before 67 | if (self.searchBar.text.length > 0) { 68 | [self.searchBar becomeFirstResponder]; 69 | [self searchBar:self.searchBar textDidChange:self.searchBar.text]; 70 | } 71 | } 72 | 73 | - (void)didReceiveMemoryWarning 74 | { 75 | [super didReceiveMemoryWarning]; 76 | // Dispose of any resources that can be recreated. 77 | } 78 | 79 | #pragma mark - UI 80 | 81 | - (void)setupFaqUI 82 | { 83 | // Search bar 84 | self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 44)]; 85 | self.searchBar.delegate = self; 86 | self.searchBar.text = self.initialSearch; 87 | self.searchBar.placeholder = [@"Search..." localizedString]; 88 | self.tableView.tableHeaderView = self.searchBar; 89 | 90 | // Nav buttons 91 | if (!self.hideContactButton) { 92 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] 93 | initWithTitle:[@"Contact" localizedString] 94 | style:UIBarButtonItemStylePlain 95 | target:self 96 | action:@selector(onContact)]; 97 | } 98 | } 99 | 100 | #pragma mark - Fetching 101 | 102 | - (void)fetchFAQs 103 | { 104 | self.tableView.hidden = YES; 105 | [self.activityView startAnimating]; 106 | [ABXFaq fetch:^(NSArray *faqs, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 107 | [self.activityView stopAnimating]; 108 | if (responseCode == ABXResponseCodeSuccess) { 109 | self.faqs = faqs; 110 | 111 | if (faqs.count == 0) { 112 | [self showError:[@"No FAQs" localizedString]]; 113 | } 114 | else { 115 | [self applySearch:self.searchBar.text]; 116 | self.tableView.hidden = NO; 117 | } 118 | } 119 | else { 120 | [self showError:[@"FAQ Error" localizedString]]; 121 | } 122 | }]; 123 | } 124 | 125 | #pragma mark - Buttons 126 | 127 | - (void)onContact 128 | { 129 | [ABXFeedbackViewController showFromController:self 130 | placeholder:[@"How can we help?" localizedString] 131 | email:nil 132 | metaData:self.contactMetaData 133 | image:nil 134 | delegate:nil]; 135 | } 136 | 137 | #pragma mark - UITableViewDataSource 138 | 139 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 140 | { 141 | return 1; 142 | } 143 | 144 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 145 | { 146 | return self.filteredFaqs.count; 147 | } 148 | 149 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 150 | { 151 | static NSString *CellIdentifier = @"FAQCell"; 152 | 153 | ABXFAQTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 154 | if (!cell) { 155 | cell = [[ABXFAQTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; 156 | } 157 | 158 | if (indexPath.row < self.filteredFaqs.count) { 159 | [cell setFAQ:[self.filteredFaqs objectAtIndex:indexPath.row]]; 160 | } 161 | 162 | return cell; 163 | } 164 | 165 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 166 | { 167 | if (indexPath.row < self.filteredFaqs.count) { 168 | return [ABXFAQTableViewCell heightForFAQ:[self.filteredFaqs objectAtIndex:indexPath.row] 169 | withWidth:CGRectGetWidth(self.tableView.bounds)]; 170 | } 171 | 172 | return 0; 173 | } 174 | 175 | #pragma mark - UITableViewDelegate 176 | 177 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 178 | { 179 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 180 | 181 | if (indexPath.row < self.filteredFaqs.count) { 182 | // Fix weird keyboard transition lag in iOS 7 183 | if ([self.searchBar isFirstResponder]) { 184 | [self.searchBar resignFirstResponder]; 185 | } 186 | 187 | // Show the details 188 | [ABXFAQViewController pushOnNavController:self.navigationController 189 | faq:self.filteredFaqs[indexPath.row] 190 | hideContactButton:self.hideContactButton]; 191 | } 192 | } 193 | 194 | #pragma mark - UISearchBarDelegate 195 | 196 | - (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar 197 | { 198 | [searchBar setShowsCancelButton:YES animated:YES]; 199 | } 200 | 201 | - (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar 202 | { 203 | [searchBar setShowsCancelButton:NO animated:YES]; 204 | } 205 | 206 | - (void)applySearch:(NSString*)searchText 207 | { 208 | if (self.faqs.count > 0) { 209 | if (searchText.length > 0) { 210 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.question contains[cd] %@ OR SELF.answer contains[cd] %@", searchText, searchText]; 211 | self.filteredFaqs = [self.faqs filteredArrayUsingPredicate:predicate]; 212 | 213 | if (self.filteredFaqs.count > 0) { 214 | self.errorLabel.hidden = YES; 215 | } 216 | else { 217 | [self showError:[@"No matches found" localizedString]]; 218 | } 219 | } 220 | else { 221 | self.filteredFaqs = self.faqs; 222 | } 223 | [self.tableView reloadData]; 224 | } 225 | } 226 | 227 | - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText 228 | { 229 | [self applySearch:searchText]; 230 | } 231 | 232 | - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar 233 | { 234 | [searchBar resignFirstResponder]; 235 | searchBar.text = @""; 236 | 237 | self.errorLabel.hidden = YES; 238 | self.filteredFaqs = self.faqs; 239 | [self.tableView reloadData]; 240 | } 241 | 242 | - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar 243 | { 244 | [searchBar resignFirstResponder]; 245 | } 246 | 247 | @end 248 | -------------------------------------------------------------------------------- /Classes/Controllers/ABXFAQViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXFAQViewController.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXFAQViewController.h" 9 | 10 | #import "ABXFaq.h" 11 | #import "ABXFeedbackViewController.h" 12 | #import "NSString+ABXLocalized.h" 13 | 14 | @interface ABXFAQViewController () 15 | 16 | @property (nonatomic, strong) UIWebView *webview; 17 | @property (nonatomic, strong) UIView *bottom; 18 | 19 | @end 20 | 21 | @implementation ABXFAQViewController 22 | 23 | - (void)dealloc 24 | { 25 | self.webview.delegate = nil; 26 | self.webview = nil; 27 | } 28 | 29 | + (void)pushOnNavController:(UINavigationController*)navigationController faq:(ABXFaq*)faq hideContactButton:(BOOL)hideContactButton 30 | { 31 | // Show the details 32 | ABXFAQViewController* controller = [[ABXFAQViewController alloc] init]; 33 | controller.faq = faq; 34 | controller.hideContactButton = hideContactButton; 35 | [navigationController pushViewController:controller animated:YES]; 36 | } 37 | 38 | 39 | - (void)viewDidLoad 40 | { 41 | [super viewDidLoad]; 42 | 43 | self.title = [@"FAQ" localizedString]; 44 | 45 | // Webview 46 | CGRect bounds = self.view.bounds; 47 | bounds.size.height -= 44; 48 | self.webview = [[UIWebView alloc] initWithFrame:bounds]; 49 | self.webview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 50 | self.webview.clipsToBounds = NO; 51 | self.webview.delegate = self; 52 | [self.view addSubview:self.webview]; 53 | 54 | [self addToolbar]; 55 | 56 | // Nav buttons 57 | if (!self.hideContactButton) { 58 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] 59 | initWithTitle:[@"Contact" localizedString] 60 | style:UIBarButtonItemStylePlain 61 | target:self 62 | action:@selector(onContact)]; 63 | } 64 | 65 | // Load the HTML 66 | NSString *html = [NSString stringWithFormat: 67 | @"" 68 | @"" 69 | @"" 75 | @"" 76 | @"" 77 | @"

%@

" 78 | @"
%@
" 79 | @"" 80 | "", self.faq.question, self.faq.answer]; 81 | [self.webview loadHTMLString:html 82 | baseURL:nil]; 83 | 84 | // Record a view, ignore the result 85 | [self.faq recordView:nil]; 86 | } 87 | 88 | - (void)didReceiveMemoryWarning 89 | { 90 | [super didReceiveMemoryWarning]; 91 | // Dispose of any resources that can be recreated.r 92 | } 93 | 94 | #pragma mark - Buttons 95 | 96 | - (void)onContact 97 | { 98 | [ABXFeedbackViewController showFromController:self 99 | placeholder:[@"How can we help?" localizedString] 100 | delegate:nil]; 101 | } 102 | 103 | #pragma mark - UI 104 | 105 | - (void)addToolbar 106 | { 107 | // Toolbar 108 | UIView *bottom = [[UIView alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.frame) - 44, CGRectGetWidth(self.view.frame), 44)]; 109 | bottom.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; 110 | bottom.backgroundColor = [UIColor whiteColor]; 111 | [self.view addSubview:bottom]; 112 | self.bottom = bottom; 113 | UIToolbar *toolbar = [[UIToolbar alloc] initWithFrame:bottom.bounds]; 114 | toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth; 115 | [bottom addSubview:toolbar]; 116 | 117 | // Voting label 118 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, 200, 44)]; 119 | label.font = [UIFont systemFontOfSize:15]; 120 | label.text = [@"Helpful?" localizedString]; 121 | label.backgroundColor = [UIColor clearColor]; 122 | [bottom addSubview:label]; 123 | 124 | // Upvote button 125 | UIButton *yesButton = [UIButton buttonWithType:UIButtonTypeSystem]; 126 | if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] == NSOrderedAscending) { 127 | yesButton.frame = CGRectMake(CGRectGetWidth(bottom.bounds) - 132, 6, 44, 32); 128 | } 129 | else { 130 | yesButton.frame = CGRectMake(CGRectGetWidth(bottom.bounds) - 132, 0, 44, 44); 131 | } 132 | yesButton.layer.cornerRadius = 4; 133 | yesButton.layer.masksToBounds = YES; 134 | [yesButton setTitle:[@"Yes" localizedString] forState:UIControlStateNormal]; 135 | yesButton.titleLabel.font = [UIFont systemFontOfSize:15]; 136 | yesButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; 137 | [yesButton addTarget:self action:@selector(onUpVote) forControlEvents:UIControlEventTouchUpInside]; 138 | [bottom addSubview:yesButton]; 139 | 140 | // Downvote button 141 | UIButton *noButton = [UIButton buttonWithType:UIButtonTypeSystem]; 142 | if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] == NSOrderedAscending) { 143 | noButton.frame = CGRectMake(CGRectGetWidth(bottom.bounds) - 66, 6, 44, 32); 144 | } 145 | else { 146 | noButton.frame = CGRectMake(CGRectGetWidth(bottom.bounds) - 66, 0, 44, 44); 147 | } 148 | noButton.layer.cornerRadius = 4; 149 | noButton.layer.masksToBounds = YES; 150 | [noButton setTitle:[@"No" localizedString] forState:UIControlStateNormal]; 151 | noButton.titleLabel.font = [UIFont systemFontOfSize:15]; 152 | noButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; 153 | [noButton addTarget:self action:@selector(onDownVote) forControlEvents:UIControlEventTouchUpInside]; 154 | [bottom addSubview:noButton]; 155 | } 156 | 157 | #pragma mark - Buttons 158 | 159 | - (void)onDone 160 | { 161 | [self.navigationController dismissViewControllerAnimated:YES completion:nil]; 162 | } 163 | 164 | #pragma mark - Voting 165 | 166 | - (void)clearBottomBar 167 | { 168 | // Remove the existing views 169 | for (UIView *v in self.bottom.subviews) { 170 | if (![v isKindOfClass:[UIToolbar class]]) { 171 | [v removeFromSuperview]; 172 | } 173 | } 174 | } 175 | 176 | - (void)showVoteLoading 177 | { 178 | [self clearBottomBar]; 179 | 180 | // Spinner 181 | UIActivityIndicatorView *activity = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; 182 | [activity startAnimating]; 183 | activity.center = CGPointMake(32, 22); 184 | [self.bottom addSubview:activity]; 185 | 186 | // Label 187 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(50, 0, 200, 44)]; 188 | label.font = [UIFont systemFontOfSize:15]; 189 | label.text = [@"One moment please..." localizedString]; 190 | label.backgroundColor = [UIColor clearColor]; 191 | [self.bottom addSubview:label]; 192 | } 193 | 194 | - (void)completeVoting 195 | { 196 | [self clearBottomBar]; 197 | 198 | // Label 199 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, 200, 44)]; 200 | label.font = [UIFont systemFontOfSize:15]; 201 | label.text = [@"Thanks for your feedback." localizedString]; 202 | label.backgroundColor = [UIColor clearColor]; 203 | [self.bottom addSubview:label]; 204 | } 205 | 206 | - (void)onDownVote 207 | { 208 | // Thumbs down 209 | [self showVoteLoading]; 210 | [self.faq downvote:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 211 | [self completeVoting]; 212 | }]; 213 | } 214 | 215 | - (void)onUpVote 216 | { 217 | // Thumbsup 218 | [self showVoteLoading]; 219 | [self.faq upvote:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 220 | [self completeVoting]; 221 | }]; 222 | 223 | } 224 | 225 | - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 226 | { 227 | if (navigationType == UIWebViewNavigationTypeLinkClicked) { 228 | [[UIApplication sharedApplication] openURL:request.URL]; 229 | return NO; 230 | } 231 | 232 | return YES; 233 | } 234 | 235 | @end 236 | -------------------------------------------------------------------------------- /Classes/Models/ABXVersion.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXVersion.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXVersion.h" 9 | 10 | #import "NSDictionary+ABXNSNullAsNull.h" 11 | #import "ABXKeychain.h" 12 | 13 | PROTECTED_ABXMODEL 14 | 15 | @implementation ABXVersion 16 | 17 | - (id)initWithAttributes:(NSDictionary*)attributes 18 | { 19 | self = [super init]; 20 | if (self) { 21 | // Date formatter, cache as they are expensive to create 22 | static dispatch_once_t onceToken; 23 | static NSDateFormatter *formatter = nil; 24 | dispatch_once(&onceToken, ^{ 25 | formatter = [NSDateFormatter new]; 26 | [formatter setDateFormat:@"yyyy-MM-dd"]; 27 | }); 28 | 29 | // Convert the string to a date 30 | NSString *releaseDateString = [attributes objectForKeyNulled:@"release_date"]; 31 | if (releaseDateString) { 32 | self.releaseDate = [formatter dateFromString:releaseDateString]; 33 | } 34 | 35 | self.version = [attributes objectForKeyNulled:@"version"]; 36 | 37 | // Look for a matching localisation 38 | if ([NSLocale preferredLanguages].count > 0) { 39 | NSString *language = [[NSLocale preferredLanguages] firstObject]; 40 | 41 | // Make sure we don't match the default language code 42 | NSString *defaultLanguage = [attributes valueForKeyPath:@"language.code"]; 43 | if (!defaultLanguage || ![language caseInsensitiveCompare:defaultLanguage] == NSOrderedSame) { 44 | // Look for an exact match 45 | [self lookForLocalisation:attributes language:language]; 46 | 47 | // Look for a partial match (e.g. match en to en-au) 48 | if (self.text == nil) { 49 | NSArray *parts = [language componentsSeparatedByString:@"-"]; 50 | if (parts.count > 1) { 51 | language = [parts firstObject]; 52 | [self lookForLocalisation:attributes language:language]; 53 | } 54 | } 55 | } 56 | } 57 | 58 | // Fall back to the default if we have nothing 59 | if (self.text == nil) { 60 | self.text = [attributes objectForKeyNulled:@"change_text"]; 61 | } 62 | } 63 | return self; 64 | } 65 | 66 | + (id)createWithAttributes:(NSDictionary*)attributes 67 | { 68 | return [[ABXVersion alloc] initWithAttributes:attributes]; 69 | } 70 | 71 | - (void)lookForLocalisation:(NSDictionary*)attributes language:(NSString*)language 72 | { 73 | for (NSDictionary *localisation in [attributes objectForKeyNulled:@"localizations"]) { 74 | NSString *languageCode = [localisation valueForKeyPath:@"language.code"]; 75 | if (languageCode && [languageCode caseInsensitiveCompare:language] == NSOrderedSame) { 76 | // Matching localisation 77 | self.text = [localisation objectForKeyNulled:@"change_text"]; 78 | break; 79 | } 80 | } 81 | } 82 | 83 | + (NSURLSessionDataTask*)fetch:(void(^)(NSArray *versions, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 84 | { 85 | return [self fetchList:@"versions" params:nil complete:complete]; 86 | } 87 | 88 | + (NSURLSessionDataTask*)fetchCurrentVersion:(void(^)(ABXVersion *version, ABXVersion *latestVersion, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete 89 | { 90 | NSString *currentVersion = NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"] ?: NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]; 91 | return [[ABXApiClient instance] GET:@"versions" 92 | params:@{ @"version" : currentVersion } 93 | complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { 94 | if (responseCode == ABXResponseCodeSuccess) { 95 | NSDictionary *results = [JSON objectForKeyNulled:@"results"]; 96 | if (results && [results isKindOfClass:[NSDictionary class]]) { 97 | // Convert into objects 98 | ABXVersion *version = nil; 99 | ABXVersion *currentVersion = nil; 100 | 101 | if ([results objectForKeyNulled:@"version"]) { 102 | version = [self createWithAttributes:[results objectForKeyNulled:@"version"]]; 103 | } 104 | 105 | if ([results objectForKeyNulled:@"current_version"]) { 106 | currentVersion = [self createWithAttributes:[results objectForKeyNulled:@"current_version"]]; 107 | } 108 | 109 | // Success! 110 | if (complete) { 111 | complete(version, currentVersion, responseCode, httpCode, error); 112 | } 113 | } 114 | else { 115 | // Decoding error, pass the values through 116 | if (complete) { 117 | complete(nil, nil, ABXResponseCodeErrorDecoding, httpCode, error); 118 | } 119 | } 120 | } 121 | else { 122 | // Error, pass the values through 123 | if (complete) { 124 | complete(nil, nil, responseCode, httpCode, error); 125 | } 126 | } 127 | }]; 128 | } 129 | 130 | - (void)markAsSeen 131 | { 132 | [[NSUserDefaults standardUserDefaults] setBool:YES forKey:[@"Version" stringByAppendingString:self.version]]; 133 | [[NSUserDefaults standardUserDefaults] synchronize]; 134 | } 135 | 136 | - (BOOL)hasSeen 137 | { 138 | return [[NSUserDefaults standardUserDefaults] boolForKey:[@"Version" stringByAppendingString:self.version]]; 139 | } 140 | 141 | - (BOOL)isNewerThanCurrent 142 | { 143 | NSString *currentVersion = NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"] ?: NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]; 144 | return [self.version compare:currentVersion options:NSNumericSearch] == NSOrderedDescending; 145 | } 146 | 147 | - (void)isLiveVersion:(NSString*)itunesId country:(NSString*)country complete:(void(^)(BOOL matches))complete 148 | { 149 | // Look up the version on the App Store and see if it matches us 150 | NSString *url = [NSString stringWithFormat:@"https://itunes.apple.com/lookup?id=%@&entity=software&country=%@", itunesId, country]; 151 | NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; 152 | [NSURLConnection sendAsynchronousRequest:request 153 | queue:[NSOperationQueue currentQueue] 154 | completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { 155 | if (data) { 156 | NSError *jsonError; 157 | NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data 158 | options:0 159 | error:&jsonError]; 160 | if (!jsonError && [result isKindOfClass:[NSDictionary class]]) { 161 | NSArray *results = [result objectForKey:@"results"]; 162 | if ([results isKindOfClass:[NSArray class]] && results.count > 0) { 163 | NSDictionary *result = [results firstObject]; 164 | NSString *storeVersion = [result objectForKey:@"version"]; 165 | if (complete && [storeVersion isKindOfClass:[NSString class]]) { 166 | complete([storeVersion isEqualToString:self.version]); 167 | return; 168 | } 169 | } 170 | } 171 | } 172 | 173 | if (complete) { 174 | complete(NO); 175 | } 176 | }]; 177 | } 178 | 179 | @end 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppbotX 2 | 3 | AppbotX is an iOS client library and [sample application](https://github.com/appbotx/appbotx/tree/master/Example) for the [AppbotX](http://appbot.co/appbotx) service. 4 | 5 | ## Requirements 6 | 7 | The [sample project](https://github.com/appbotx/appbotx/tree/master/Example) includes a test key, but for you own application you will need an [Appbot](http://appbot.co) account and an API key. 8 | 9 | ## Installation 10 | 11 | Appbotx will be available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile and run pod install. 12 | 13 | pod "AppbotX", :git => "https://github.com/appbotx/appbotx.git" 14 | 15 | Alternatively you can just [download the latest release](https://github.com/appbotx/appbotx/releases) and add it to your project. 16 | 17 | Then initialize with your API key in your AppDelegate 18 | 19 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 20 | { 21 | [[ABXApiClient instance] setApiKey:@"API_KEY"]; 22 | return YES; 23 | } 24 | 25 | And import ABX.h into your precompiled header. Alternatively you can just include it within the files you require. 26 | 27 | #ifdef __OBJC__ 28 | #import 29 | #import 30 | 31 | #import "ABX.h" 32 | #endif 33 | 34 | ## Usage 35 | 36 | To run the example project; clone the repo, and open "Sample Project.xcodeproj" from the [Example folder](https://github.com/appbotx/appbotx/tree/master/Example). 37 | 38 | ### FAQ 39 | 40 | #### Default UI 41 | To show the default UI simply call the showFromController helper method on ABXFAQsViewController. 42 | 43 | [ABXFAQsViewController showFromController:self hideContactButton:NO contactMetaData:nil initialSearch:nil]; 44 | 45 | * **controller** - required - the controller to be presented from. 46 | * **hideContactButton** - YES/NO - if the contact button should be shown the the top right. 47 | * **metaData** - optional - extra meta data you would like to attach if the contact is shown, only use types supported by NSJSONSerialization, e.g. NSString, NSNumber etc. 48 | * **initialSearch** - optional - the initial search filter to apply to the results. 49 | 50 | #### Push On Your Own UINavigationController 51 | 52 | ABXFAQsViewController *controller = [[ABXFAQsViewController alloc] init]; 53 | // Optinally set hideContactButton & contactMetaData 54 | [self.navigationController pushViewController:controller animated:YES]; 55 | 56 | #### Fetch Manually 57 | 58 | [ABXFaq fetch:^(NSArray *faqs, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 59 | switch (responseCode) { 60 | case ABXResponseCodeSuccess: { 61 | // Success, use faqs 62 | } 63 | break; 64 | 65 | default: { 66 | // Failure 67 | } 68 | break; 69 | } 70 | }]; 71 | 72 | * **faqs** array of ABXFaq objects. 73 | * **responseCode** - response code, ABXResponseCodeSuccess for success, see enum for errors. 74 | * **httpCode** - the http code, 200 for success etc. 75 | * **error** - the error, nil if success. 76 | 77 | --- 78 | 79 | ### Feedback 80 | 81 | #### Default UI 82 | 83 | To show the default UI simply call the showFromController helper method on ABXFeedbackViewController. 84 | 85 | [ABXFeedbackViewController showFromController:self placeholder:@"default hint" email:nil metaData:@{ @"Sample" : @YES } image:nil]; 86 | 87 | * **controller** - required - the controller to be presented from. 88 | * **placeholder** - optional - the default hint text shown, nil to use the default. 89 | * **email** - optional - the default email address to use, if you have this otherwise nil. 90 | * **metaData** - optional - extra meta data you would like to attach, only use types supported by NSJSONSerialization, e.g. NSString, NSNumber etc. 91 | * **image** - optional - an image, such as a screenshot to be attached by default. 92 | 93 | #### Push On Your Own UINavigationController 94 | 95 | ABXFeedbackViewController *controller = [[ABXFeedbackViewController alloc] init]; 96 | [self.navigationController pushViewController:controller animated:YES]; 97 | 98 | --- 99 | 100 | ### Versions 101 | 102 | #### Default UI 103 | 104 | Showing a list: 105 | 106 | [ABXVersionsViewController showFromController:self]; 107 | 108 | * **controller** - required - the controller to be presented from. 109 | 110 | Showing new versions / update text: 111 | 112 | [ABXVersionNotificationView fetchAndShowInController:self foriTunesID:kiTunesID backgroundColor:[UIColor redColor] textColor:[UIColor blackColor] buttonColor:[UIColor whiteColor] complete:^(BOOL shown) { 113 | // Here you may want to chain fetching notifications 114 | // if it wasn't shown 115 | }]; 116 | 117 | * **controller** - required - the controller to be presented from. 118 | * **foriTunesID** - required - your iTunes identifier. 119 | * **backgroundColor** - required - the background color for the control. 120 | * **textColor** - required - the text color for the control. 121 | * **buttonColor** - required - the color for the buttons. 122 | * **complete** - optional - a callback when the operation has completed. 123 | 124 | #### Manually Fetching 125 | 126 | Fetch the list of versions: 127 | 128 | [ABXVersion fetch:^(NSArray *versions, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 129 | switch (responseCode) { 130 | case ABXResponseCodeSuccess: { 131 | } 132 | break; 133 | 134 | default: { 135 | } 136 | break; 137 | } 138 | }]; 139 | 140 | * **versions** array of ABXVersion objects. 141 | * **responseCode** - response code, ABXResponseCodeSuccess for success, see enum for errors. 142 | * **httpCode** - the http code, 200 for success etc. 143 | * **error** - the error, nil if success. 144 | 145 | Fetch the current version: 146 | 147 | [ABXVersion fetchCurrentVersion:^(ABXVersion *version, ABXVersion *currentVersion, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 148 | }]; 149 | 150 | * **version** the version matching the current build version. 151 | * **currentVersion** the latest version on the server. 152 | * **responseCode** - response code, ABXResponseCodeSuccess for success, see enum for errors. 153 | * **httpCode** - the http code, 200 for success etc. 154 | * **error** - the error, nil if success. 155 | 156 | --- 157 | 158 | ### Notifications 159 | 160 | #### Default UI 161 | 162 | Showing a list: 163 | 164 | [ABXNotificationsViewController showFromController:self]; 165 | 166 | * **controller** - required - the controller to be presented from. 167 | 168 | Showing the current active notification: 169 | 170 | [ABXNotificationView fetchAndShowInController:self 171 | backgroundColor:[UIColor blackColor] textColor:[UIColor whiteColor] buttonColor:[UIColor blueColor] complete:^(BOOL shown) { 172 | }]; 173 | 174 | * **controller** - required - the controller to be presented from. 175 | * **backgroundColor** - required - the background color for the control. 176 | * **textColor** - required - the text color for the control. 177 | * **buttonColor** - required - the color for the buttons. 178 | * **complete** - optional - a callback when the operation has completed. 179 | 180 | #### Manually Fetching 181 | 182 | Fetch the list of notifications: 183 | 184 | [ABXNotification fetch:^(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 185 | switch (responseCode) { 186 | case ABXResponseCodeSuccess: { 187 | } 188 | break; 189 | 190 | default: { 191 | } 192 | break; 193 | } 194 | }]; 195 | 196 | * **notifications** array of ABXNotification objects. 197 | * **responseCode** - response code, ABXResponseCodeSuccess for success, see enum for errors. 198 | * **httpCode** - the http code, 200 for success etc. 199 | * **error** - the error, nil if success. 200 | 201 | Fetch the active notification: 202 | 203 | [ABXNotification fetchActive:^(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { 204 | switch (responseCode) { 205 | }]; 206 | 207 | * **notifications** array of ABXNotification objects, currently only ever one. 208 | * **responseCode** - response code, ABXResponseCodeSuccess for success, see enum for errors. 209 | * **httpCode** - the http code, 200 for success etc. 210 | * **error** - the error, nil if success. 211 | 212 | ## Localizing Strings 213 | 214 | You can change any of the default strings by using localization. All strings used can be found in AppbotX.bundle, but it will prefer strings declared in your local strings bundle. 215 | 216 | e.g. 217 | 218 | "How can we help?" = "How can we help you with Sample App?"; 219 | 220 | See the [sample app](https://github.com/appbotx/appbotx/tree/master/Example) for an example. 221 | 222 | ## Review Prompt 223 | 224 | Add a property to store the ABXPromptView 225 | 226 | #import "ABXPromptView.h" 227 | @interface YourViewController () 228 | @property (nonatomic, strong) ABXPromptView *promptView; 229 | @end 230 | 231 | Add it to your view dynamically: 232 | 233 | // The prompt view is an example workflow using AppbotX 234 | // It's also good to only show it after a positive interaction 235 | // or a number of usages of the app 236 | if (![ABXPromptView hasHadInteractionForCurrentVersion]) { 237 | self.promptView = [[ABXPromptView alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.bounds) - 100, CGRectGetWidth(self.view.bounds), 100)]; 238 | self.promptView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth; 239 | [self.view addSubview:self.promptView]; 240 | self.promptView.delegate = self; 241 | } 242 | 243 | Implement the delegate: 244 | 245 | #pragma mark - ABXPromptViewDelegate 246 | 247 | - (void)appbotPromptForReview { 248 | [ABXAppStore openAppStoreReviewForApp:kiTunesID]; 249 | self.promptView.hidden = YES; 250 | } 251 | 252 | - (void)appbotPromptForFeedback { 253 | [ABXFeedbackViewController showFromController:self placeholder:nil]; 254 | self.promptView.hidden = YES; 255 | } 256 | 257 | - (void)appbotPromptClose { 258 | self.promptView.hidden = YES; 259 | } 260 | 261 | ## Communication 262 | 263 | * If you found a bug, [open an issue](https://github.com/appbotx/appbotx/issues). 264 | * If you have a feature request, [open an issue](https://github.com/appbotx/appbotx/issues). 265 | * If you want to contribute, [submit a pull request](https://github.com/appbotx/appbotx/pulls). 266 | 267 | ## License 268 | 269 | AppbotX is available under the MIT license. See the LICENSE file for more info. 270 | 271 | -------------------------------------------------------------------------------- /Classes/Classes/ABXKeychain.m: -------------------------------------------------------------------------------- 1 | // 2 | // Altered Version of https://github.com/nicklockwood/FXKeychain 3 | // Altered to not conflict with any existing uses of the library 4 | // 5 | // FXKeychain.m 6 | // 7 | // Version 1.5 beta 8 | // 9 | // Created by Nick Lockwood on 29/12/2012. 10 | // Copyright 2012 Charcoal Design 11 | // 12 | // Distributed under the permissive zlib License 13 | // Get the latest version from here: 14 | // 15 | // https://github.com/nicklockwood/FXKeychain 16 | // 17 | // This software is provided 'as-is', without any express or implied 18 | // warranty. In no event will the authors be held liable for any damages 19 | // arising from the use of this software. 20 | // 21 | // Permission is granted to anyone to use this software for any purpose, 22 | // including commercial applications, and to alter it and redistribute it 23 | // freely, subject to the following restrictions: 24 | // 25 | // 1. The origin of this software must not be misrepresented; you must not 26 | // claim that you wrote the original software. If you use this software 27 | // in a product, an acknowledgment in the product documentation would be 28 | // appreciated but is not required. 29 | // 30 | // 2. Altered source versions must be plainly marked as such, and must not be 31 | // misrepresented as being the original software. 32 | // 33 | // 3. This notice may not be removed or altered from any source distribution. 34 | // 35 | 36 | #import "ABXKeychain.h" 37 | 38 | #import 39 | #if !__has_feature(objc_arc) 40 | #error This class requires automatic reference counting 41 | #endif 42 | 43 | 44 | @implementation NSObject (AppBotKeychainPropertyListCoding) 45 | 46 | - (id)AppBotKeychain_propertyListRepresentation 47 | { 48 | return self; 49 | } 50 | 51 | @end 52 | 53 | #if !APPBOTKEYCHAIN_USE_NSCODING 54 | 55 | @implementation NSNull (AppBotKeychainPropertyListCoding) 56 | 57 | - (id)AppBotKeychain_propertyListRepresentation 58 | { 59 | return nil; 60 | } 61 | 62 | @end 63 | 64 | 65 | @implementation NSArray (BMPropertyListCoding) 66 | 67 | - (id)AppBotKeychain_propertyListRepresentation 68 | { 69 | NSMutableArray *copy = [NSMutableArray arrayWithCapacity:[self count]]; 70 | [self enumerateObjectsUsingBlock:^(__unsafe_unretained id obj, __unused NSUInteger idx, __unused BOOL *stop) { 71 | id value = [obj AppBotKeychain_propertyListRepresentation]; 72 | if (value) [copy addObject:value]; 73 | }]; 74 | return copy; 75 | } 76 | 77 | @end 78 | 79 | 80 | @implementation NSDictionary (BMPropertyListCoding) 81 | 82 | - (id)AppBotKeychain_propertyListRepresentation 83 | { 84 | NSMutableDictionary *copy = [NSMutableDictionary dictionaryWithCapacity:[self count]]; 85 | [self enumerateKeysAndObjectsUsingBlock:^(__unsafe_unretained id key, __unsafe_unretained id obj, __unused BOOL *stop) { 86 | id value = [obj AppBotKeychain_propertyListRepresentation]; 87 | if (value) copy[key] = value; 88 | }]; 89 | return copy; 90 | } 91 | 92 | @end 93 | 94 | #endif 95 | 96 | @implementation ABXKeychain 97 | 98 | + (instancetype)defaultKeychain 99 | { 100 | static id sharedInstance = nil; 101 | static dispatch_once_t onceToken; 102 | dispatch_once(&onceToken, ^{ 103 | 104 | NSString *bundleID = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey]; 105 | sharedInstance = [[ABXKeychain alloc] initWithService:bundleID 106 | accessGroup:nil]; 107 | }); 108 | 109 | return sharedInstance; 110 | } 111 | 112 | - (id)init 113 | { 114 | return [self initWithService:nil accessGroup:nil]; 115 | } 116 | 117 | - (id)initWithService:(NSString *)service 118 | accessGroup:(NSString *)accessGroup 119 | { 120 | return [self initWithService:service 121 | accessGroup:accessGroup 122 | accessibility:ABXKeychainAccessibleWhenUnlocked]; 123 | } 124 | 125 | - (id)initWithService:(NSString *)service 126 | accessGroup:(NSString *)accessGroup 127 | accessibility:(ABXKeychainAccess)accessibility 128 | { 129 | if ((self = [super init])) 130 | { 131 | _service = [service copy]; 132 | _accessGroup = [accessGroup copy]; 133 | _accessibility = accessibility; 134 | } 135 | return self; 136 | } 137 | 138 | - (NSData *)dataForKey:(id)key 139 | { 140 | //generate query 141 | NSMutableDictionary *query = [NSMutableDictionary dictionary]; 142 | if ([self.service length]) query[(__bridge NSString *)kSecAttrService] = self.service; 143 | query[(__bridge NSString *)kSecClass] = (__bridge id)kSecClassGenericPassword; 144 | query[(__bridge NSString *)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne; 145 | query[(__bridge NSString *)kSecReturnData] = (__bridge id)kCFBooleanTrue; 146 | query[(__bridge NSString *)kSecAttrAccount] = [key description]; 147 | 148 | #if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR 149 | if ([_accessGroup length]) query[(__bridge NSString *)kSecAttrAccessGroup] = _accessGroup; 150 | #endif 151 | 152 | //recover data 153 | CFDataRef data = NULL; 154 | OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&data); 155 | if (status != errSecSuccess && status != errSecItemNotFound) 156 | { 157 | NSLog(@"AppBotKeychain failed to retrieve data for key '%@', error: %ld", key, (long)status); 158 | } 159 | return CFBridgingRelease(data); 160 | } 161 | 162 | - (BOOL)setObject:(id)object forKey:(id)key 163 | { 164 | NSParameterAssert(key); 165 | 166 | //generate query 167 | NSMutableDictionary *query = [NSMutableDictionary dictionary]; 168 | if ([self.service length]) query[(__bridge NSString *)kSecAttrService] = self.service; 169 | query[(__bridge NSString *)kSecClass] = (__bridge id)kSecClassGenericPassword; 170 | query[(__bridge NSString *)kSecAttrAccount] = [key description]; 171 | 172 | #if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR 173 | if ([_accessGroup length]) query[(__bridge NSString *)kSecAttrAccessGroup] = _accessGroup; 174 | #endif 175 | 176 | //encode object 177 | NSData *data = nil; 178 | NSError *error = nil; 179 | if ([(id)object isKindOfClass:[NSString class]]) 180 | { 181 | //check that string data does not represent a binary plist 182 | NSPropertyListFormat format = NSPropertyListBinaryFormat_v1_0; 183 | if (![object hasPrefix:@"bplist"] || ![NSPropertyListSerialization propertyListWithData:[object dataUsingEncoding:NSUTF8StringEncoding] 184 | options:NSPropertyListImmutable 185 | format:&format 186 | error:NULL]) 187 | { 188 | //safe to encode as a string 189 | data = [object dataUsingEncoding:NSUTF8StringEncoding]; 190 | } 191 | } 192 | 193 | //if not encoded as a string, encode as plist 194 | if (object && !data) 195 | { 196 | data = [NSPropertyListSerialization dataWithPropertyList:[object AppBotKeychain_propertyListRepresentation] 197 | format:NSPropertyListBinaryFormat_v1_0 198 | options:0 199 | error:&error]; 200 | #if APPBOTKEYCHAIN_USE_NSCODING 201 | 202 | //property list encoding failed. try NSCoding 203 | if (!data) 204 | { 205 | data = [NSKeyedArchiver archivedDataWithRootObject:object]; 206 | } 207 | 208 | #endif 209 | 210 | } 211 | 212 | //fail if object is invalid 213 | NSAssert(!object || (object && data), @"AppBotKeychain failed to encode object for key '%@', error: %@", key, error); 214 | 215 | if (data) 216 | { 217 | //update values 218 | NSMutableDictionary *update = [@{(__bridge NSString *)kSecValueData: data} mutableCopy]; 219 | 220 | #if TARGET_OS_IPHONE || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9 221 | 222 | update[(__bridge NSString *)kSecAttrAccessible] = @[(__bridge id)kSecAttrAccessibleWhenUnlocked, 223 | (__bridge id)kSecAttrAccessibleAfterFirstUnlock, 224 | (__bridge id)kSecAttrAccessibleAlways, 225 | (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly, 226 | (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, 227 | (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly][self.accessibility]; 228 | #endif 229 | 230 | //write data 231 | OSStatus status = errSecSuccess; 232 | if ([self dataForKey:key]) 233 | { 234 | //there's already existing data for this key, update it 235 | status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update); 236 | } 237 | else 238 | { 239 | //no existing data, add a new item 240 | [query addEntriesFromDictionary:update]; 241 | status = SecItemAdd ((__bridge CFDictionaryRef)query, NULL); 242 | } 243 | if (status != errSecSuccess) 244 | { 245 | NSLog(@"AppBotKeychain failed to store data for key '%@', error: %ld", key, (long)status); 246 | return NO; 247 | } 248 | } 249 | else 250 | { 251 | //delete existing data 252 | 253 | #if TARGET_OS_IPHONE 254 | 255 | OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); 256 | #else 257 | CFTypeRef result = NULL; 258 | query[(__bridge id)kSecReturnRef] = (__bridge id)kCFBooleanTrue; 259 | OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); 260 | if (status == errSecSuccess) 261 | { 262 | status = SecKeychainItemDelete((SecKeychainItemRef) result); 263 | CFRelease(result); 264 | } 265 | #endif 266 | if (status != errSecSuccess) 267 | { 268 | NSLog(@"AppBotKeychain failed to delete data for key '%@', error: %ld", key, (long)status); 269 | return NO; 270 | } 271 | } 272 | return YES; 273 | } 274 | 275 | - (BOOL)setObject:(id)object forKeyedSubscript:(id)key 276 | { 277 | return [self setObject:object forKey:key]; 278 | } 279 | 280 | - (BOOL)removeObjectForKey:(id)key 281 | { 282 | return [self setObject:nil forKey:key]; 283 | } 284 | 285 | - (id)objectForKey:(id)key 286 | { 287 | NSData *data = [self dataForKey:key]; 288 | if (data) 289 | { 290 | id object = nil; 291 | NSError *error = nil; 292 | NSPropertyListFormat format = NSPropertyListBinaryFormat_v1_0; 293 | 294 | //check if data is a binary plist 295 | if ([data length] >= 6 && !strncmp("bplist", data.bytes, 6)) 296 | { 297 | //attempt to decode as a plist 298 | object = [NSPropertyListSerialization propertyListWithData:data 299 | options:NSPropertyListImmutable 300 | format:&format 301 | error:&error]; 302 | 303 | if ([object respondsToSelector:@selector(objectForKey:)] && object[@"$archiver"]) 304 | { 305 | //data represents an NSCoded archive 306 | 307 | #if APPBOTKEYCHAIN_USE_NSCODING 308 | 309 | //parse as archive 310 | object = [NSKeyedUnarchiver unarchiveObjectWithData:data]; 311 | #else 312 | //don't trust it 313 | object = nil; 314 | #endif 315 | 316 | } 317 | } 318 | if (!object || format != NSPropertyListBinaryFormat_v1_0) 319 | { 320 | //may be a string 321 | object = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 322 | } 323 | if (!object) 324 | { 325 | NSLog(@"AppBotKeychain failed to decode data for key '%@', error: %@", key, error); 326 | } 327 | return object; 328 | } 329 | else 330 | { 331 | //no value found 332 | return nil; 333 | } 334 | } 335 | 336 | - (id)objectForKeyedSubscript:(id)key 337 | { 338 | return [self objectForKey:key]; 339 | } 340 | 341 | @end 342 | -------------------------------------------------------------------------------- /Classes/Classes/ABXApiClient.m: -------------------------------------------------------------------------------- 1 | // 2 | // ABXApiClient.m 3 | // 4 | // Created by Stuart Hall on 21/05/2014. 5 | // Copyright (c) 2014 Appbot. All rights reserved. 6 | // 7 | 8 | #import "ABXApiClient.h" 9 | 10 | #import "NSDictionary+ABXQueryString.h" 11 | #import "NSDictionary+ABXNSNullAsNull.h" 12 | 13 | #import 14 | #import 15 | 16 | @interface ABXApiClient () 17 | 18 | @property (nonatomic, strong) NSURLSession *session; 19 | @property (nonatomic, strong) NSOperationQueue *queue; 20 | 21 | @property (nonatomic, copy) NSString *apiKey; 22 | 23 | @end 24 | 25 | @implementation ABXApiClient 26 | 27 | static NSString *kAppbotUrl = @"https://api.appbot.co/v1"; 28 | 29 | + (ABXApiClient*)instance 30 | { 31 | static dispatch_once_t onceToken; 32 | static ABXApiClient *client = nil; 33 | dispatch_once(&onceToken, ^{ 34 | client = [[ABXApiClient alloc] init]; 35 | }); 36 | return client; 37 | } 38 | 39 | #pragma mark - Api Key 40 | 41 | - (void)setApiKey:(NSString *)apiKey 42 | { 43 | _apiKey = apiKey; 44 | 45 | // Initialise 46 | [self GET:@"app" 47 | params:[self combineDefaultParamsWith:@{}] 48 | complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { 49 | }]; 50 | } 51 | 52 | #pragma mark - Init 53 | 54 | - (id)init 55 | { 56 | self = [super init]; 57 | if (self) { 58 | // Setup the request queue 59 | self.queue = [[NSOperationQueue alloc] init]; 60 | _queue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount; 61 | 62 | // Setup our session 63 | self.session = [NSURLSession sessionWithConfiguration:nil delegate:nil delegateQueue:_queue]; 64 | } 65 | return self; 66 | } 67 | 68 | #pragma mark - Requests 69 | 70 | - (NSURLSessionDataTask*)GET:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete 71 | { 72 | NSDictionary *parameters = [self combineDefaultParamsWith:params]; 73 | 74 | // Create our URL 75 | NSURL *url = [[NSURL URLWithString:kAppbotUrl] URLByAppendingPathComponent:path]; 76 | NSString *query = [parameters queryStringValue]; 77 | url = [NSURL URLWithString:[[url absoluteString] stringByAppendingFormat:url.query ? @"&%@" : @"?%@", query]]; 78 | 79 | // Create the request 80 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 81 | request.allowsCellularAccess = YES; 82 | request.HTTPMethod = @"GET"; 83 | return [self performRequest:request complete:complete]; 84 | } 85 | 86 | - (NSURLSessionDataTask*)POST:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete 87 | { 88 | return [self httpBodyRequest:@"POST" path:path params:params complete:complete]; 89 | } 90 | 91 | - (NSURLSessionDataTask*)PUT:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete 92 | { 93 | return [self httpBodyRequest:@"PUT" path:path params:params complete:complete]; 94 | } 95 | 96 | - (NSURLSessionDataTask*)POSTImage:(NSString*)path image:(UIImage*)image complete:(ABXRequestCompletion)complete 97 | { 98 | NSDictionary *parameters = [self combineDefaultParamsWith:@{}]; 99 | 100 | // Create our URL 101 | NSURL *url = [[NSURL URLWithString:kAppbotUrl] URLByAppendingPathComponent:path]; 102 | NSString *query = [parameters queryStringValue]; 103 | url = [NSURL URLWithString:[[url absoluteString] stringByAppendingFormat:url.query ? @"&%@" : @"?%@", query]]; 104 | 105 | // Request 106 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 107 | request.allowsCellularAccess = YES; 108 | request.HTTPMethod = @"POST"; 109 | 110 | // Boundary 111 | NSString *boundary = @"0Xdfdfegsdfsd6fRD"; 112 | NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; 113 | [request addValue:contentType forHTTPHeaderField: @"Content-Type"]; 114 | 115 | // Attachment body 116 | NSMutableData *body = [NSMutableData data]; 117 | [body appendData:[[NSString stringWithFormat:@"--%@\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]]; 118 | [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file\"; filename=\"%@.png\"\r\n", [[NSUUID UUID] UUIDString]] dataUsingEncoding:NSUTF8StringEncoding]]; 119 | [body appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; 120 | [body appendData:UIImagePNGRepresentation(image)]; 121 | [body appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; 122 | [request setHTTPBody:body]; 123 | 124 | return [self performRequest:request complete:complete]; 125 | } 126 | 127 | - (NSURLSessionDataTask*)httpBodyRequest:(NSString*)method path:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete 128 | { 129 | NSDictionary *parameters = [self combineDefaultParamsWith:params]; 130 | 131 | // Create our URL 132 | NSURL *url = [[NSURL URLWithString:kAppbotUrl] URLByAppendingPathComponent:path]; 133 | NSString *charset = (__bridge NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)); 134 | 135 | // Create the request 136 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 137 | [request setValue:[NSString stringWithFormat:@"application/json; charset=%@", charset] forHTTPHeaderField:@"Content-Type"]; 138 | NSError *error = nil; 139 | [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:parameters options:0 error:&error]]; 140 | if (error) { 141 | // Error setting the HTTP body 142 | if (complete) { 143 | complete(ABXResponseCodeErrorEncoding, -1, error, nil); 144 | } 145 | return nil; 146 | } 147 | else { 148 | request.allowsCellularAccess = YES; 149 | request.HTTPMethod = method; 150 | return [self performRequest:request complete:complete]; 151 | } 152 | } 153 | 154 | - (NSURLSessionDataTask*)performRequest:(NSURLRequest*)request complete:(ABXRequestCompletion)complete 155 | { 156 | // https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html#//apple_ref/doc/uid/TP40013174-CH14-SW1 157 | if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { 158 | // iOS 6.1 and below 159 | [NSURLConnection sendAsynchronousRequest:request 160 | queue:self.queue 161 | completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { 162 | [self handleResponse:response data:data error:error complete:complete]; 163 | }]; 164 | return nil; 165 | } 166 | else { 167 | // iOS 7 168 | NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request 169 | completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 170 | [self handleResponse:response data:data error:error complete:complete]; }]; 171 | [task resume]; 172 | return task; 173 | } 174 | } 175 | 176 | - (void)handleResponse:(NSURLResponse*)response data:(NSData*)data error:(NSError*)error complete:(ABXRequestCompletion)complete 177 | { 178 | NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; 179 | NSInteger httpCode = [httpResponse statusCode]; 180 | if (httpCode >= 200 && httpCode < 300) { 181 | [self handleRequestSuccess:httpCode data:data complete:complete]; 182 | } 183 | else { 184 | [self handleRequestFailure:httpCode error:error complete:complete]; 185 | } 186 | } 187 | 188 | - (void)handleRequestSuccess:(NSInteger)httpCode data:(NSData*)data complete:(ABXRequestCompletion)complete 189 | { 190 | NSError *jsonError = nil; 191 | NSDictionary *json = nil; 192 | 193 | if (data != nil && data.length > 0) { 194 | json = [NSJSONSerialization JSONObjectWithData:data 195 | options:0 196 | error:&jsonError]; 197 | } 198 | 199 | if (jsonError) { 200 | // JSON error 201 | if (complete) { 202 | dispatch_async(dispatch_get_main_queue(), ^{ 203 | complete(ABXResponseCodeErrorDecoding, httpCode, jsonError, nil); 204 | }); 205 | } 206 | } 207 | else { 208 | // Success! 209 | if (complete) { 210 | dispatch_async(dispatch_get_main_queue(), ^{ 211 | complete(ABXResponseCodeSuccess, httpCode, nil, json); 212 | }); 213 | } 214 | } 215 | } 216 | 217 | - (void)handleRequestFailure:(NSInteger)httpCode error:(NSError*)error complete:(ABXRequestCompletion)complete 218 | { 219 | // Work out which error code 220 | ABXResponseCode responseCode = ABXResponseCodeErrorUnknown; 221 | switch (httpCode) { 222 | case 401: 223 | responseCode = ABXResponseCodeErrorAuth; 224 | break; 225 | 226 | case 402: 227 | responseCode = ABXResponseCodeErrorExpired; 228 | break; 229 | } 230 | 231 | if (complete) { 232 | dispatch_async(dispatch_get_main_queue(), ^{ 233 | complete(responseCode, httpCode, error, nil); 234 | }); 235 | } 236 | } 237 | 238 | #pragma mark - Key 239 | 240 | - (void)validateApiKey 241 | { 242 | // The API key must always be set 243 | assert(_apiKey); 244 | if (_apiKey == nil || _apiKey.length == 0) { 245 | NSException* myException = [NSException 246 | exceptionWithName:@"InvalidKeyException" 247 | reason:@"Key is not valid." 248 | userInfo:nil]; 249 | @throw myException; 250 | } 251 | } 252 | 253 | #pragma mark - Params 254 | 255 | - (NSDictionary*)combineDefaultParamsWith:(NSDictionary*)params 256 | { 257 | [self validateApiKey]; 258 | 259 | NSDictionary *defaultParams = @{ @"bundle_identifier" : [[NSBundle mainBundle] bundleIdentifier], 260 | @"key" : _apiKey }; 261 | if (params == nil) { 262 | // If there are no other params just use they key and bundle 263 | return defaultParams; 264 | } 265 | else { 266 | // Append the default values 267 | NSMutableDictionary *mutableParams = [params mutableCopy]; 268 | [mutableParams addEntriesFromDictionary:defaultParams]; 269 | return mutableParams; 270 | } 271 | } 272 | 273 | + (BOOL)isInternetReachable 274 | { 275 | // http://stackoverflow.com/a/18071526 276 | struct sockaddr_in zeroAddress; 277 | bzero(&zeroAddress, sizeof(zeroAddress)); 278 | zeroAddress.sin_len = sizeof(zeroAddress); 279 | zeroAddress.sin_family = AF_INET; 280 | 281 | SCNetworkReachabilityRef reachabilityRef = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *) &zeroAddress); 282 | 283 | SCNetworkReachabilityFlags flags; 284 | if (reachabilityRef && SCNetworkReachabilityGetFlags(reachabilityRef, &flags)) { 285 | if ((flags & kSCNetworkReachabilityFlagsReachable) == 0) { 286 | // if target host is not reachable 287 | CFRelease(reachabilityRef); 288 | return NO; 289 | } 290 | 291 | if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) { 292 | // if target host is reachable and no connection is required 293 | // then we'll assume (for now) that your on Wi-Fi 294 | CFRelease(reachabilityRef); 295 | return YES; // This is a wifi connection. 296 | } 297 | 298 | 299 | if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) 300 | ||(flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)) { 301 | // ... and the connection is on-demand (or on-traffic) if the 302 | // calling application is using the CFSocketStream or higher APIs 303 | 304 | if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) { 305 | // ... and no [user] intervention is needed 306 | CFRelease(reachabilityRef); 307 | return YES; // This is a wifi connection. 308 | } 309 | } 310 | 311 | if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN) { 312 | // ... but WWAN connections are OK if the calling application 313 | // is using the CFNetwork (CFSocketStream?) APIs. 314 | CFRelease(reachabilityRef); 315 | return YES; // This is a cellular connection. 316 | } 317 | } 318 | 319 | return NO; 320 | } 321 | 322 | @end 323 | -------------------------------------------------------------------------------- /Example/Sample Project/Storyboard.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 38 | 48 | 58 | 68 | 78 | 88 | 98 | 108 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | --------------------------------------------------------------------------------