├── 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 |
--------------------------------------------------------------------------------