From e5b3cbd00c7bb4c759da16414ca0861898ef0cc0 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 7 Aug 2018 15:36:34 -0600 Subject: [PATCH] Use StoreKit for reviews --- Signal.xcodeproj/project.pbxproj | 27 +- Signal/Libraries/iRate/iRate.h | 173 --- Signal/Libraries/iRate/iRate.m | 1140 ----------------- Signal/src/AppDelegate.m | 4 - .../ConversationViewController.m | 3 + .../HomeView/HomeViewController.m | 31 +- Signal/src/util/AppStoreRating.h | 10 - Signal/src/util/AppStoreRating.m | 32 - .../translations/en.lproj/Localizable.strings | 9 - .../UIColor+JSQMessages.h | 2 +- 10 files changed, 34 insertions(+), 1397 deletions(-) delete mode 100755 Signal/Libraries/iRate/iRate.h delete mode 100755 Signal/Libraries/iRate/iRate.m delete mode 100644 Signal/src/util/AppStoreRating.h delete mode 100644 Signal/src/util/AppStoreRating.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 78f25d347..5f17093e8 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -42,7 +42,6 @@ 340FC8CD20518C77007AEB0F /* OWSBackupJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8CC20518C76007AEB0F /* OWSBackupJob.m */; }; 340FC8D0205BF2FA007AEB0F /* OWSBackupIO.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8CE205BF2FA007AEB0F /* OWSBackupIO.m */; }; 341F2C0F1F2B8AE700D07D6B /* DebugUIMisc.m in Sources */ = {isa = PBXBuildFile; fileRef = 341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */; }; - 34219801210612F600C57195 /* iRate.m in Sources */ = {isa = PBXBuildFile; fileRef = 342197FF210612F600C57195 /* iRate.m */; }; 3421980F21061A0700C57195 /* UIColor+JSQMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 3421980521061A0600C57195 /* UIColor+JSQMessages.m */; }; 3421981021061A0700C57195 /* JSQMessagesAvatarImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 3421980621061A0600C57195 /* JSQMessagesAvatarImage.m */; }; 3421981121061A0700C57195 /* JSQMessagesAvatarImageFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 3421980721061A0600C57195 /* JSQMessagesAvatarImageFactory.h */; }; @@ -440,6 +439,7 @@ 4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; }; 4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; }; 4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */; }; + 4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; }; 768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; }; 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; @@ -484,13 +484,11 @@ B660F6DB1C29868000687D6E /* FunctionalUtilTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B660F6AD1C29868000687D6E /* FunctionalUtilTest.m */; }; B660F6E01C29868000687D6E /* UtilTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B660F6B41C29868000687D6E /* UtilTest.m */; }; B660F7561C29988E00687D6E /* PushManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B9ECFB198B31BA00C620D3 /* PushManager.m */; }; - B660F7721C29988E00687D6E /* AppStoreRating.m in Sources */ = {isa = PBXBuildFile; fileRef = B6DA6B061B8A2F9A00CA6F98 /* AppStoreRating.m */; }; B66DBF4A19D5BBC8006EA940 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; }; B67EBF5D19194AC60084CCFD /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B67EBF5C19194AC60084CCFD /* Settings.bundle */; }; B69CD25119773E79005CE69A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B69CD25019773E79005CE69A /* XCTest.framework */; }; B6B226971BE4B7D200860F4D /* ContactsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6B226961BE4B7D200860F4D /* ContactsUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; B6B9ECFC198B31BA00C620D3 /* PushManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B9ECFB198B31BA00C620D3 /* PushManager.m */; }; - B6DA6B071B8A2F9A00CA6F98 /* AppStoreRating.m in Sources */ = {isa = PBXBuildFile; fileRef = B6DA6B061B8A2F9A00CA6F98 /* AppStoreRating.m */; }; B6F509971AA53F760068F56A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; }; B6FE7EB71ADD62FA00A6D22F /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6FE7EB61ADD62FA00A6D22F /* PushKit.framework */; }; B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; }; @@ -661,8 +659,6 @@ 341458471FBE11C4005ABCF9 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = translations/fa.lproj/Localizable.strings; sourceTree = ""; }; 341F2C0D1F2B8AE700D07D6B /* DebugUIMisc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMisc.h; sourceTree = ""; }; 341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMisc.m; sourceTree = ""; }; - 342197FF210612F600C57195 /* iRate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = iRate.m; sourceTree = ""; }; - 34219800210612F600C57195 /* iRate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iRate.h; sourceTree = ""; }; 3421980521061A0600C57195 /* UIColor+JSQMessages.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+JSQMessages.m"; sourceTree = ""; }; 3421980621061A0600C57195 /* JSQMessagesAvatarImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQMessagesAvatarImage.m; sourceTree = ""; }; 3421980721061A0600C57195 /* JSQMessagesAvatarImageFactory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQMessagesAvatarImageFactory.h; sourceTree = ""; }; @@ -1121,6 +1117,7 @@ 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalUBSan.supp; sourceTree = ""; }; 4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = ""; }; 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationConfigurationSyncOperation.swift; sourceTree = ""; }; + 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuActionsViewController.swift; sourceTree = ""; }; 69349DE607F5BA6036C9AC60 /* Pods-SignalShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.debug.xcconfig"; sourceTree = ""; }; 70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; @@ -1212,8 +1209,6 @@ B6B9ECFA198B31BA00C620D3 /* PushManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PushManager.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; B6B9ECFB198B31BA00C620D3 /* PushManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PushManager.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; B6BC3D0C1AA544B100C2907F /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = translations/da.lproj/Localizable.strings; sourceTree = ""; }; - B6DA6B051B8A2F9A00CA6F98 /* AppStoreRating.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppStoreRating.h; sourceTree = ""; }; - B6DA6B061B8A2F9A00CA6F98 /* AppStoreRating.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppStoreRating.m; sourceTree = ""; }; B6F509961AA53F760068F56A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = translations/en.lproj/Localizable.strings; sourceTree = ""; }; B6FE7EB61ADD62FA00A6D22F /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = System/Library/Frameworks/PushKit.framework; sourceTree = SDKROOT; }; B90418E4183E9DD40038554A /* DateUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DateUtil.h; sourceTree = ""; }; @@ -1272,6 +1267,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */, 455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */, 455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */, 45847E871E4283C30080EAB3 /* Intents.framework in Frameworks */, @@ -1446,20 +1442,9 @@ path = ThreadSettings; sourceTree = ""; }; - 342197FE210612F600C57195 /* iRate */ = { - isa = PBXGroup; - children = ( - 342197FF210612F600C57195 /* iRate.m */, - 34219800210612F600C57195 /* iRate.h */, - ); - name = iRate; - path = Signal/Libraries/iRate; - sourceTree = SOURCE_ROOT; - }; 34219802210619C800C57195 /* Libraries */ = { isa = PBXGroup; children = ( - 342197FE210612F600C57195 /* iRate */, ); path = Libraries; sourceTree = ""; @@ -2189,8 +2174,6 @@ 76EB04C818170B33006006FC /* util */ = { isa = PBXGroup; children = ( - B6DA6B051B8A2F9A00CA6F98 /* AppStoreRating.h */, - B6DA6B061B8A2F9A00CA6F98 /* AppStoreRating.m */, 34CCAF361F0C0599004084F4 /* AppUpdateNag.h */, 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */, B90418E4183E9DD40038554A /* DateUtil.h */, @@ -2413,6 +2396,7 @@ D221A08C169C9E5E00537ABF /* Frameworks */ = { isa = PBXGroup; children = ( + 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */, 455A16DB1F1FEA0000F86704 /* Metal.framework */, 455A16DC1F1FEA0000F86704 /* MetalKit.framework */, 45847E861E4283C30080EAB3 /* Intents.framework */, @@ -3295,7 +3279,6 @@ 454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */, 340FC8B4204DAC8D007AEB0F /* OWSBackupSettingsViewController.m in Sources */, 34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */, - B6DA6B071B8A2F9A00CA6F98 /* AppStoreRating.m in Sources */, 451A13B11E13DED2000A50FD /* CallNotificationsAdapter.swift in Sources */, 348570A820F67575004FF32B /* OWSMessageHeaderView.m in Sources */, 450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */, @@ -3380,7 +3363,6 @@ 3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */, 34B3F8751E8DF1700035BE1A /* CallViewController.swift in Sources */, 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */, - 34219801210612F600C57195 /* iRate.m in Sources */, 45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */, 34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */, 34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */, @@ -3469,7 +3451,6 @@ 34DB0BED2011548B007B313F /* OWSDatabaseConverterTest.m in Sources */, 45360B911F952AA900FA666C /* MarqueeLabel.swift in Sources */, 454EBAB41F2BE14C00ACE0BB /* OWSAnalytics.swift in Sources */, - B660F7721C29988E00687D6E /* AppStoreRating.m in Sources */, 954AEE6A1DF33E01002E5410 /* ContactsPickerTest.swift in Sources */, 45666F581D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m in Sources */, B660F6E01C29868000687D6E /* UtilTest.m in Sources */, diff --git a/Signal/Libraries/iRate/iRate.h b/Signal/Libraries/iRate/iRate.h deleted file mode 100755 index 46ac19158..000000000 --- a/Signal/Libraries/iRate/iRate.h +++ /dev/null @@ -1,173 +0,0 @@ -// -// iRate.h -// -// Version 1.11.4 -// -// Created by Nick Lockwood on 26/01/2011. -// Copyright 2011 Charcoal Design -// -// Distributed under the permissive zlib license -// Get the latest version from here: -// -// https://github.com/nicklockwood/iRate -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// -// 3. This notice may not be removed or altered from any source distribution. -// - - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wobjc-missing-property-synthesis" - - -#import -#undef weak_delegate -#if __has_feature(objc_arc_weak) && (TARGET_OS_IPHONE || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_8) -#define weak_delegate weak -#else -#define weak_delegate unsafe_unretained -#endif - - -#import -#if TARGET_OS_IPHONE -#import -#define IRATE_EXTERN UIKIT_EXTERN -#else -#import -#define IRATE_EXTERN APPKIT_EXTERN -#endif - -//! Project version number for iRate. -FOUNDATION_EXPORT double iRateVersionNumber; - -//! Project version string for iRate. -FOUNDATION_EXPORT const unsigned char iRateVersionString[]; - -IRATE_EXTERN NSUInteger const iRateAppStoreGameGenreID; -IRATE_EXTERN NSString *const iRateErrorDomain; - -// localisation string keys -IRATE_EXTERN NSString *const iRateMessageTitleKey; // iRateMessageTitle -IRATE_EXTERN NSString *const iRateAppMessageKey; // iRateAppMessage -IRATE_EXTERN NSString *const iRateGameMessageKey; // iRateGameMessage -IRATE_EXTERN NSString *const iRateUpdateMessageKey; // iRateUpdateMessage -IRATE_EXTERN NSString *const iRateCancelButtonKey; // iRateCancelButton -IRATE_EXTERN NSString *const iRateRemindButtonKey; // iRateRemindButton -IRATE_EXTERN NSString *const iRateRateButtonKey; // iRateRateButton - -// notification keys -IRATE_EXTERN NSString *const iRateCouldNotConnectToAppStore; -IRATE_EXTERN NSString *const iRateDidDetectAppUpdate; -IRATE_EXTERN NSString *const iRateDidPromptForRating; -IRATE_EXTERN NSString *const iRateUserDidAttemptToRateApp; -IRATE_EXTERN NSString *const iRateUserDidDeclineToRateApp; -IRATE_EXTERN NSString *const iRateUserDidRequestReminderToRateApp; -IRATE_EXTERN NSString *const iRateDidOpenAppStore; - - -typedef NS_ENUM(NSUInteger, iRateErrorCode) { - iRateErrorBundleIdDoesNotMatchAppStore = 1, - iRateErrorApplicationNotFoundOnAppStore, - iRateErrorApplicationIsNotLatestVersion, - iRateErrorCouldNotOpenRatingPageURL -}; - - -@protocol iRateDelegate -@optional - -- (void)iRateCouldNotConnectToAppStore:(NSError *)error; -- (void)iRateDidDetectAppUpdate; -- (BOOL)iRateShouldPromptForRating; -- (void)iRateDidPromptForRating; -- (void)iRateUserDidAttemptToRateApp; -- (void)iRateUserDidDeclineToRateApp; -- (void)iRateUserDidRequestReminderToRateApp; -- (BOOL)iRateShouldOpenAppStore; -- (void)iRateDidOpenAppStore; - -@end - - -@interface iRate : NSObject - -+ (instancetype)sharedInstance; - -// app store ID - this is only needed if your -// bundle ID is not unique between iOS and Mac app stores -@property (nonatomic, assign) NSUInteger appStoreID; - -// application details - these are set automatically -@property (nonatomic, assign) NSUInteger appStoreGenreID; -@property (nonatomic, copy) NSString *appStoreCountry; -@property (nonatomic, copy) NSString *applicationName; -@property (nonatomic, copy) NSString *applicationVersion; -@property (nonatomic, copy) NSString *applicationBundleID; - -// usage settings - these have sensible defaults -@property (nonatomic, assign) NSUInteger usesUntilPrompt; -@property (nonatomic, assign) NSUInteger eventsUntilPrompt; -@property (nonatomic, assign) float daysUntilPrompt; -@property (nonatomic, assign) float usesPerWeekForPrompt; -@property (nonatomic, assign) float remindPeriod; - -// message text, you may wish to customise these -@property (nonatomic, copy) NSString *messageTitle; -@property (nonatomic, copy) NSString *message; -@property (nonatomic, copy) NSString *updateMessage; -@property (nonatomic, copy) NSString *cancelButtonLabel; -@property (nonatomic, copy) NSString *remindButtonLabel; -@property (nonatomic, copy) NSString *rateButtonLabel; - -// debugging and prompt overrides -@property (nonatomic, assign) BOOL useUIAlertControllerIfAvailable; -@property (nonatomic, assign) BOOL useAllAvailableLanguages; -@property (nonatomic, assign) BOOL promptForNewVersionIfUserRated; -@property (nonatomic, assign) BOOL onlyPromptIfLatestVersion; -@property (nonatomic, assign) BOOL onlyPromptIfMainWindowIsAvailable; -@property (nonatomic, assign) BOOL promptAtLaunch; -@property (nonatomic, assign) BOOL verboseLogging; -@property (nonatomic, assign) BOOL previewMode; - -// advanced properties for implementing custom behaviour -@property (nonatomic, strong) NSURL *ratingsURL; -@property (nonatomic, strong) NSDate *firstUsed; -@property (nonatomic, strong) NSDate *lastReminded; -@property (nonatomic, assign) NSUInteger usesCount; -@property (nonatomic, assign) NSUInteger eventCount; -@property (nonatomic, readonly) float usesPerWeek; -@property (nonatomic, assign) BOOL declinedThisVersion; -@property (nonatomic, readonly) BOOL declinedAnyVersion; -@property (nonatomic, assign) BOOL ratedThisVersion; -@property (nonatomic, readonly) BOOL ratedAnyVersion; -@property (nonatomic, weak_delegate) id delegate; - -// manually control behaviour -- (BOOL)shouldPromptForRating; -- (void)promptForRating; -- (void)promptIfNetworkAvailable; -- (BOOL)promptIfAllCriteriaMet; -- (void)openRatingsPageInAppStore; -- (void)logEvent:(BOOL)deferPrompt; -- (void)preventPromptAtNextTest; - -@end - - -#pragma clang diagnostic pop diff --git a/Signal/Libraries/iRate/iRate.m b/Signal/Libraries/iRate/iRate.m deleted file mode 100755 index 47732e37c..000000000 --- a/Signal/Libraries/iRate/iRate.m +++ /dev/null @@ -1,1140 +0,0 @@ -// -// iRate.m -// -// Version 1.11.4 -// -// Created by Nick Lockwood on 26/01/2011. -// Copyright 2011 Charcoal Design -// -// Distributed under the permissive zlib license -// Get the latest version from here: -// -// https://github.com/nicklockwood/iRate -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// -// 3. This notice may not be removed or altered from any source distribution. -// - - -#import "iRate.h" - - -#import -#if !__has_feature(objc_arc) -#error This class requires automatic reference counting -#endif - -// Unknown diagnostic -// #pragma clang diagnostic ignored "-Wreceiver-is-weak" -#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak" -#pragma clang diagnostic ignored "-Wobjc-missing-property-synthesis" -#pragma clang diagnostic ignored "-Wdirect-ivar-access" -#pragma clang diagnostic ignored "-Wunused-macros" -#pragma clang diagnostic ignored "-Wconversion" -#pragma clang diagnostic ignored "-Wformat-nonliteral" -#pragma clang diagnostic ignored "-Wselector" -#pragma clang diagnostic ignored "-Wgnu" - - -NSUInteger const iRateAppStoreGameGenreID = 6014; -NSString *const iRateErrorDomain = @"iRateErrorDomain"; - - -NSString *const iRateMessageTitleKey = @"iRateMessageTitle"; -NSString *const iRateAppMessageKey = @"iRateAppMessage"; -NSString *const iRateGameMessageKey = @"iRateGameMessage"; -NSString *const iRateUpdateMessageKey = @"iRateUpdateMessage"; -NSString *const iRateCancelButtonKey = @"iRateCancelButton"; -NSString *const iRateRemindButtonKey = @"iRateRemindButton"; -NSString *const iRateRateButtonKey = @"iRateRateButton"; - -NSString *const iRateCouldNotConnectToAppStore = @"iRateCouldNotConnectToAppStore"; -NSString *const iRateDidDetectAppUpdate = @"iRateDidDetectAppUpdate"; -NSString *const iRateDidPromptForRating = @"iRateDidPromptForRating"; -NSString *const iRateUserDidAttemptToRateApp = @"iRateUserDidAttemptToRateApp"; -NSString *const iRateUserDidDeclineToRateApp = @"iRateUserDidDeclineToRateApp"; -NSString *const iRateUserDidRequestReminderToRateApp = @"iRateUserDidRequestReminderToRateApp"; -NSString *const iRateDidOpenAppStore = @"iRateDidOpenAppStore"; - -static NSString *const iRateAppStoreIDKey = @"iRateAppStoreID"; -static NSString *const iRateRatedVersionKey = @"iRateRatedVersionChecked"; -static NSString *const iRateDeclinedVersionKey = @"iRateDeclinedVersion"; -static NSString *const iRateLastRemindedKey = @"iRateLastReminded"; -static NSString *const iRateLastVersionUsedKey = @"iRateLastVersionUsed"; -static NSString *const iRateFirstUsedKey = @"iRateFirstUsed"; -static NSString *const iRateUseCountKey = @"iRateUseCount"; -static NSString *const iRateEventCountKey = @"iRateEventCount"; - -static NSString *const iRateMacAppStoreBundleID = @"com.apple.appstore"; -static NSString *const iRateAppLookupURLFormat = @"https://itunes.apple.com/%@/lookup"; - -static NSString *const iRateiOSAppStoreURLScheme = @"itms-apps"; -static NSString *const iRateiOSAppStoreURLFormat = @"itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/" - @"viewContentsUserReviews?type=Purple+Software&id=%@&pageNumber=0&" - @"sortOrdering=2&mt=8"; -static NSString *const iRateiOS7AppStoreURLFormat = @"itms-apps://itunes.apple.com/app/id%@"; -static NSString *const iRateMacAppStoreURLFormat = @"macappstore://itunes.apple.com/app/id%@"; - - -#define SECONDS_IN_A_DAY 86400.0 -#define SECONDS_IN_A_WEEK 604800.0 -#define MAC_APP_STORE_REFRESH_DELAY 5.0 -#define REQUEST_TIMEOUT 60.0 - - -@implementation NSObject (iRate) - -- (void)iRateCouldNotConnectToAppStore:(__unused NSError *)error { -} -- (void)iRateDidDetectAppUpdate { -} -- (BOOL)iRateShouldPromptForRating { - return YES; -} -- (void)iRateDidPromptForRating { -} -- (void)iRateUserDidAttemptToRateApp { -} -- (void)iRateUserDidDeclineToRateApp { -} -- (void)iRateUserDidRequestReminderToRateApp { -} -- (BOOL)iRateShouldOpenAppStore { - return YES; -} -- (void)iRateDidOpenAppStore { -} - -@end - - -@interface iRate () - -@property (nonatomic, strong) id visibleAlert; -@property (nonatomic, assign) BOOL checkingForPrompt; -@property (nonatomic, assign) BOOL checkingForAppStoreID; -@property (nonatomic, assign) BOOL shouldPreventPromptAtNextTest; -@property (nonatomic) BOOL hasPromptedAtLaunch; -@property (nonatomic) BOOL hasQueuedSynchronize; - -@end - - -@implementation iRate - -// SIGNAL HACK -// Disabled in Signal. We don't want to prompt users who haven't registered, even if it has been -// a while since they installed. ~mjk -//+ (void)load { -// [self performSelectorOnMainThread:@selector(sharedInstance) withObject:nil waitUntilDone:NO]; -//} - -+ (instancetype)sharedInstance { - static iRate *sharedInstance = nil; - if (sharedInstance == nil) { - sharedInstance = [(iRate *)[self alloc] init]; - } - return sharedInstance; -} - -- (NSString *)localizedStringForKey:(NSString *)key withDefault:(NSString *)defaultString { - static NSBundle *bundle = nil; - if (bundle == nil) { - NSString *bundlePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"iRate" ofType:@"bundle"]; - if (self.useAllAvailableLanguages) { - bundle = [NSBundle bundleWithPath:bundlePath]; - NSString *language = [[NSLocale preferredLanguages] count] ? [NSLocale preferredLanguages][0] : @"en"; - if (![[bundle localizations] containsObject:language]) { - language = [language componentsSeparatedByString:@"-"][0]; - } - if ([[bundle localizations] containsObject:language]) { - bundlePath = [bundle pathForResource:language ofType:@"lproj"]; - } - } - bundle = [NSBundle bundleWithPath:bundlePath] ?: [NSBundle mainBundle]; - } - defaultString = [bundle localizedStringForKey:key value:defaultString table:nil]; - return [[NSBundle mainBundle] localizedStringForKey:key value:defaultString table:nil]; -} - -- (iRate *)init { - if ((self = [super init])) { -#if TARGET_OS_IPHONE - - // register for iphone application events - if (&UIApplicationWillEnterForegroundNotification) { - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillEnterForeground) - name:UIApplicationWillEnterForegroundNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationDidBecomeActive) - name:UIApplicationDidBecomeActiveNotification - object:nil]; - } - -#endif - - // get country - self.appStoreCountry = [(NSLocale *)[NSLocale currentLocale] objectForKey:NSLocaleCountryCode]; - if ([self.appStoreCountry isEqualToString:@"150"]) { - self.appStoreCountry = @"eu"; - } else if (!self.appStoreCountry || - [[self.appStoreCountry stringByReplacingOccurrencesOfString:@"[A-Za-z]{2}" - withString:@"" - options:NSRegularExpressionSearch - range:NSMakeRange(0, 2)] length]) { - self.appStoreCountry = @"us"; - } - - // application version (use short version preferentially) - self.applicationVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; - if ([self.applicationVersion length] == 0) { - self.applicationVersion = - [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]; - } - - // localised application name - self.applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; - if ([self.applicationName length] == 0) { - self.applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey]; - } - - // bundle id - self.applicationBundleID = [[NSBundle mainBundle] bundleIdentifier]; - - // default settings - self.useAllAvailableLanguages = YES; - self.promptForNewVersionIfUserRated = NO; - self.onlyPromptIfLatestVersion = YES; - self.onlyPromptIfMainWindowIsAvailable = YES; - self.promptAtLaunch = YES; - self.usesUntilPrompt = 10; - self.eventsUntilPrompt = 10; - self.daysUntilPrompt = 10.0f; - self.usesPerWeekForPrompt = 0.0f; - self.remindPeriod = 1.0f; - self.verboseLogging = NO; - self.previewMode = NO; - self.shouldPreventPromptAtNextTest = NO; - -#if DEBUG - - // enable verbose logging in debug mode - self.verboseLogging = YES; - NSLog(@"iRate verbose logging enabled."); - -#endif - - // app launched - [self performSelectorOnMainThread:@selector(applicationLaunched) withObject:nil waitUntilDone:NO]; - } - return self; -} - -- (id)delegate { - if (_delegate == nil) { -#if TARGET_OS_IPHONE -#define APP_CLASS UIApplication -#else -#define APP_CLASS NSApplication -#endif - - _delegate = (id)[[APP_CLASS sharedApplication] delegate]; - } - return _delegate; -} - -- (NSString *)messageTitle { - return [_messageTitle ?: [self localizedStringForKey:iRateMessageTitleKey withDefault:@"Rate %@"] - stringByReplacingOccurrencesOfString:@"%@" - withString:self.applicationName]; -} - -- (NSString *)message { - NSString *message = _message; - if (!message) { - message = (self.appStoreGenreID == iRateAppStoreGameGenreID) - ? [self localizedStringForKey:iRateGameMessageKey - withDefault:@"If you enjoy playing %@, would you mind taking a moment to rate " - @"it? It won’t take more than a minute. Thanks for your support!"] - : [self localizedStringForKey:iRateAppMessageKey - withDefault:@"If you enjoy using %@, would you mind taking a moment to rate " - @"it? It won’t take more than a minute. Thanks for your support!"]; - } - return [message stringByReplacingOccurrencesOfString:@"%@" withString:self.applicationName]; -} - -- (NSString *)updateMessage { - NSString *updateMessage = _updateMessage; - if (!updateMessage) { - updateMessage = [self localizedStringForKey:iRateUpdateMessageKey withDefault:self.message]; - } - return [updateMessage stringByReplacingOccurrencesOfString:@"%@" withString:self.applicationName]; -} - -- (NSString *)cancelButtonLabel { - return _cancelButtonLabel ?: [self localizedStringForKey:iRateCancelButtonKey withDefault:@"No, Thanks"]; -} - -- (NSString *)rateButtonLabel { - return _rateButtonLabel ?: [self localizedStringForKey:iRateRateButtonKey withDefault:@"Rate It Now"]; -} - -- (NSString *)remindButtonLabel { - return _remindButtonLabel ?: [self localizedStringForKey:iRateRemindButtonKey withDefault:@"Remind Me Later"]; -} - -- (NSURL *)ratingsURL { - if (_ratingsURL) { - return _ratingsURL; - } - - if (!self.appStoreID && self.verboseLogging) { - NSLog(@"iRate could not find the App Store ID for this application. If the application is not intended for App " - @"Store release then you must specify a custom ratingsURL."); - } - - NSString *URLString; - -#if TARGET_OS_IPHONE - - float iOSVersion = [[UIDevice currentDevice].systemVersion floatValue]; - if (iOSVersion >= 7.0f && iOSVersion < 7.1f) { - URLString = iRateiOS7AppStoreURLFormat; - } else { - URLString = iRateiOSAppStoreURLFormat; - } - -#else - - URLString = iRateMacAppStoreURLFormat; - -#endif - - return [NSURL URLWithString:[NSString stringWithFormat:URLString, @(self.appStoreID)]]; -} - -- (NSUInteger)appStoreID { - return _appStoreID - ?: [[[NSUserDefaults standardUserDefaults] objectForKey:iRateAppStoreIDKey] unsignedIntegerValue]; -} - -- (NSDate *)firstUsed { - return [[NSUserDefaults standardUserDefaults] objectForKey:iRateFirstUsedKey]; -} - -- (void)setFirstUsed:(NSDate *)date { - [[NSUserDefaults standardUserDefaults] setObject:date forKey:iRateFirstUsedKey]; - [self synchronizeWhenSafeToDoSo]; -} - -- (NSDate *)lastReminded { - return [[NSUserDefaults standardUserDefaults] objectForKey:iRateLastRemindedKey]; -} - -- (void)setLastReminded:(NSDate *)date { - [[NSUserDefaults standardUserDefaults] setObject:date forKey:iRateLastRemindedKey]; - [self synchronizeWhenSafeToDoSo]; -} - -- (NSUInteger)usesCount { - return [[NSUserDefaults standardUserDefaults] integerForKey:iRateUseCountKey]; -} - -- (void)setUsesCount:(NSUInteger)count { - [[NSUserDefaults standardUserDefaults] setInteger:(NSInteger)count forKey:iRateUseCountKey]; - [self synchronizeWhenSafeToDoSo]; -} - -- (NSUInteger)eventCount { - return [[NSUserDefaults standardUserDefaults] integerForKey:iRateEventCountKey]; -} - -- (void)setEventCount:(NSUInteger)count { - [[NSUserDefaults standardUserDefaults] setInteger:(NSInteger)count forKey:iRateEventCountKey]; - [self synchronizeWhenSafeToDoSo]; -} - -- (float)usesPerWeek { - return (float)self.usesCount / ([[NSDate date] timeIntervalSinceDate:self.firstUsed] / SECONDS_IN_A_WEEK); -} - -- (BOOL)declinedThisVersion { - return [[[NSUserDefaults standardUserDefaults] objectForKey:iRateDeclinedVersionKey] - isEqualToString:self.applicationVersion]; -} - -- (void)setDeclinedThisVersion:(BOOL)declined { - [[NSUserDefaults standardUserDefaults] setObject:(declined ? self.applicationVersion : nil) - forKey:iRateDeclinedVersionKey]; - [self synchronizeWhenSafeToDoSo]; -} - -- (BOOL)declinedAnyVersion { - return [(NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:iRateDeclinedVersionKey] length] != 0; -} - -- (BOOL)ratedVersion:(NSString *)version { - return [[[NSUserDefaults standardUserDefaults] objectForKey:iRateRatedVersionKey] isEqualToString:version]; -} - -- (BOOL)ratedThisVersion { - return [self ratedVersion:self.applicationVersion]; -} - -- (void)setRatedThisVersion:(BOOL)rated { - [[NSUserDefaults standardUserDefaults] setObject:(rated ? self.applicationVersion : nil) - forKey:iRateRatedVersionKey]; - [self synchronizeWhenSafeToDoSo]; -} - -- (BOOL)ratedAnyVersion { - return [(NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:iRateRatedVersionKey] length] != 0; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (void)preventPromptAtNextTest { - self.shouldPreventPromptAtNextTest = YES; -} - -- (void)incrementUseCount { - self.usesCount++; -} - -- (void)incrementEventCount { - self.eventCount++; -} - -- (BOOL)shouldPromptForRating { - // Avoid potentially inconvenient prompt - if (self.shouldPreventPromptAtNextTest) - { - NSLog(@"iRate did not prompt for rating because it is potentially inconvenient"); - self.shouldPreventPromptAtNextTest = NO; - return NO; - } - - // preview mode? - if (self.previewMode) { - NSLog(@"iRate preview mode is enabled - make sure you disable this for release"); - return YES; - } - - // check if we've rated this version - else if (self.ratedThisVersion) { - if (self.verboseLogging) { - NSLog(@"iRate did not prompt for rating because the user has already rated this version"); - } - return NO; - } - - // check if we've rated any version - else if (self.ratedAnyVersion && !self.promptForNewVersionIfUserRated) { - if (self.verboseLogging) { - NSLog(@"iRate did not prompt for rating because the user has already rated this app, and " - @"promptForNewVersionIfUserRated is disabled"); - } - return NO; - } - - // check if we've declined to rate the app - else if (self.declinedAnyVersion) { - if (self.verboseLogging) { - NSLog(@"iRate did not prompt for rating because the user has declined to rate the app"); - } - return NO; - } - - // check how long we've been using this version - else if ([[NSDate date] timeIntervalSinceDate:self.firstUsed] < self.daysUntilPrompt * SECONDS_IN_A_DAY) { - if (self.verboseLogging) { - NSLog(@"iRate did not prompt for rating because the app was first used less than %g days ago", - self.daysUntilPrompt); - } - return NO; - } - - // check how many times we've used it and the number of significant events - else if (self.usesCount < self.usesUntilPrompt && self.eventCount < self.eventsUntilPrompt) { - if (self.verboseLogging) { - NSLog(@"iRate did not prompt for rating because the app has only been used %@ times and only %@ events " - @"have been logged", - @(self.usesCount), - @(self.eventCount)); - } - return NO; - } - - // check if usage frequency is high enough - else if (self.usesPerWeek < self.usesPerWeekForPrompt) { - if (self.verboseLogging) { - NSLog(@"iRate did not prompt for rating because the app has only been used %g times per week on average " - @"since it was installed", - self.usesPerWeek); - } - return NO; - } - - // check if within the reminder period - else if (self.lastReminded != nil && - [[NSDate date] timeIntervalSinceDate:self.lastReminded] < self.remindPeriod * SECONDS_IN_A_DAY) { - if (self.verboseLogging) { - NSLog(@"iRate did not prompt for rating because the user last asked to be reminded less than %g days ago", - self.remindPeriod); - } - return NO; - } - - // lets prompt! - return YES; -} - -- (NSString *)valueForKey:(NSString *)key inJSON:(id)json { - if ([json isKindOfClass:[NSString class]]) { - // use legacy parser - NSRange keyRange = [json rangeOfString:[NSString stringWithFormat:@"\"%@\"", key]]; - if (keyRange.location != NSNotFound) { - NSInteger start = keyRange.location + keyRange.length; - NSRange valueStart = [json rangeOfString:@":" - options:(NSStringCompareOptions)0 - range:NSMakeRange(start, [(NSString *)json length] - start)]; - if (valueStart.location != NSNotFound) { - start = valueStart.location + 1; - NSRange valueEnd = [json rangeOfString:@"," - options:(NSStringCompareOptions)0 - range:NSMakeRange(start, [(NSString *)json length] - start)]; - if (valueEnd.location != NSNotFound) { - NSString *value = [json substringWithRange:NSMakeRange(start, valueEnd.location - start)]; - value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - while ([value hasPrefix:@"\""] && ![value hasSuffix:@"\""]) { - if (valueEnd.location == NSNotFound) { - break; - } - NSInteger newStart = valueEnd.location + 1; - valueEnd = [json rangeOfString:@"," - options:(NSStringCompareOptions)0 - range:NSMakeRange(newStart, [(NSString *)json length] - newStart)]; - value = [json substringWithRange:NSMakeRange(start, valueEnd.location - start)]; - value = - [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - } - - value = [value - stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\""]]; - value = [value stringByReplacingOccurrencesOfString:@"\\\\" withString:@"\\"]; - value = [value stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"]; - value = [value stringByReplacingOccurrencesOfString:@"\\\"" withString:@"\""]; - value = [value stringByReplacingOccurrencesOfString:@"\\n" withString:@"\n"]; - value = [value stringByReplacingOccurrencesOfString:@"\\r" withString:@"\r"]; - value = [value stringByReplacingOccurrencesOfString:@"\\t" withString:@"\t"]; - value = [value stringByReplacingOccurrencesOfString:@"\\f" withString:@"\f"]; - value = [value stringByReplacingOccurrencesOfString:@"\\b" withString:@"\f"]; - - while (YES) { - NSRange unicode = [value rangeOfString:@"\\u"]; - if (unicode.location == NSNotFound || unicode.location + unicode.length == 0) { - break; - } - - uint32_t c = 0; - NSString *hex = [value substringWithRange:NSMakeRange(unicode.location + 2, 4)]; - NSScanner *scanner = [NSScanner scannerWithString:hex]; - [scanner scanHexInt:&c]; - - if (c <= 0xffff) { - value = [value - stringByReplacingCharactersInRange:NSMakeRange(unicode.location, 6) - withString:[NSString stringWithFormat:@"%C", (unichar)c]]; - } else { - // convert character to surrogate pair - uint16_t x = (uint16_t)c; - uint16_t u = (c >> 16) & ((1 << 5) - 1); - uint16_t w = (uint16_t)u - 1; - unichar high = 0xd800 | (w << 6) | x >> 10; - unichar low = (uint16_t)(0xdc00 | (x & ((1 << 10) - 1))); - - value = [value - stringByReplacingCharactersInRange:NSMakeRange(unicode.location, 6) - withString:[NSString stringWithFormat:@"%C%C", high, low]]; - } - } - return value; - } - } - } - } else { - return json[key]; - } - return nil; -} - -- (void)setAppStoreIDOnMainThread:(NSString *)appStoreIDString { - _appStoreID = [appStoreIDString integerValue]; - [[NSUserDefaults standardUserDefaults] setInteger:_appStoreID forKey:iRateAppStoreIDKey]; - [self synchronizeWhenSafeToDoSo]; -} - -- (void)connectionSucceeded { - if (self.checkingForAppStoreID) { - // no longer checking - self.checkingForPrompt = NO; - self.checkingForAppStoreID = NO; - - // open app store - [self openRatingsPageInAppStore]; - } else if (self.checkingForPrompt) { - // no longer checking - self.checkingForPrompt = NO; - - // confirm with delegate - if (![self.delegate iRateShouldPromptForRating]) { - if (self.verboseLogging) { - NSLog(@"iRate did not display the rating prompt because the iRateShouldPromptForRating delegate method " - @"returned NO"); - } - return; - } - - // prompt user - [self promptForRating]; - } -} - -- (void)connectionError:(NSError *)error { - if (self.checkingForPrompt || self.checkingForAppStoreID) { - // no longer checking - self.checkingForPrompt = NO; - self.checkingForAppStoreID = NO; - - // log the error - if (error) { - NSLog(@"iRate rating process failed because: %@", [error localizedDescription]); - } else { - NSLog(@"iRate rating process failed because an unknown error occured"); - } - - // could not connect - [self.delegate iRateCouldNotConnectToAppStore:error]; - [[NSNotificationCenter defaultCenter] postNotificationName:iRateCouldNotConnectToAppStore object:error]; - } -} - -- (void)checkForConnectivityInBackground { - if ([NSThread isMainThread]) { - [self performSelectorInBackground:@selector(checkForConnectivityInBackground) withObject:nil]; - return; - } - - @autoreleasepool { - // prevent concurrent checks - static BOOL checking = NO; - if (checking) - return; - checking = YES; - - // first check iTunes - NSString *iTunesServiceURL = [NSString stringWithFormat:iRateAppLookupURLFormat, self.appStoreCountry]; - if (_appStoreID) // important that we check ivar and not getter in case it has changed - { - iTunesServiceURL = [iTunesServiceURL stringByAppendingFormat:@"?id=%@", @(_appStoreID)]; - } else { - iTunesServiceURL = [iTunesServiceURL stringByAppendingFormat:@"?bundleId=%@", self.applicationBundleID]; - } - - if (self.verboseLogging) { - NSLog(@"iRate is checking %@ to retrieve the App Store details...", iTunesServiceURL); - } - - NSError *error = nil; - NSURLResponse *response = nil; - NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:iTunesServiceURL] - cachePolicy:NSURLRequestUseProtocolCachePolicy - timeoutInterval:REQUEST_TIMEOUT]; - NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; - NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; - if (data && statusCode == 200) { - // in case error is garbage... - error = nil; - - id json = nil; - if ([NSJSONSerialization class]) { - json = [[NSJSONSerialization JSONObjectWithData:data options:(NSJSONReadingOptions)0 - error:&error][@"results"] lastObject]; - } else { - // convert to string - json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - } - - if (!error) { - NSLog(@"JSON: %@", json); - // check bundle ID matches - NSString *bundleID = [self valueForKey:@"bundleId" inJSON:json]; - if (bundleID) { - if ([bundleID isEqualToString:self.applicationBundleID]) { - // get genre - if (self.appStoreGenreID == 0) { - self.appStoreGenreID = [[self valueForKey:@"primaryGenreId" inJSON:json] integerValue]; - } - - // get app id - if (!_appStoreID) { - NSString *appStoreIDString = [self valueForKey:@"trackId" inJSON:json]; - [self performSelectorOnMainThread:@selector(setAppStoreIDOnMainThread:) - withObject:appStoreIDString - waitUntilDone:YES]; - - if (self.verboseLogging) { - NSLog(@"iRate found the app on iTunes. The App Store ID is %@", appStoreIDString); - } - } - - // check version - if (self.onlyPromptIfLatestVersion && !self.previewMode) { - NSString *latestVersion = [self valueForKey:@"version" inJSON:json]; - if ([latestVersion compare:self.applicationVersion options:NSNumericSearch] == - NSOrderedDescending) { - if (self.verboseLogging) { - NSLog(@"iRate found that the installed application version (%@) is not the latest " - @"version on the App Store, which is %@", - self.applicationVersion, - latestVersion); - } - - error = [NSError errorWithDomain:iRateErrorDomain - code:iRateErrorApplicationIsNotLatestVersion - userInfo:@{ - NSLocalizedDescriptionKey : - @"Installed app is not the latest version available" - }]; - } - } - } else { - if (self.verboseLogging) { - NSLog(@"iRate found that the application bundle ID (%@) does not match the bundle ID of " - @"the app found on iTunes (%@) with the specified App Store ID (%@)", - self.applicationBundleID, - bundleID, - @(self.appStoreID)); - } - - error = [NSError - errorWithDomain:iRateErrorDomain - code:iRateErrorBundleIdDoesNotMatchAppStore - userInfo:@{ - NSLocalizedDescriptionKey : [NSString - stringWithFormat: - @"Application bundle ID does not match expected value of %@", bundleID] - }]; - } - } else if (_appStoreID || !self.ratingsURL) { - if (self.verboseLogging) { - NSLog(@"iRate could not find this application on iTunes. If your app is not intended for App " - @"Store release then you must specify a custom ratingsURL. If this is the first release " - @"of your application then it's not a problem that it cannot be found on the store yet"); - } - if (!self.previewMode) { - error = [NSError errorWithDomain:iRateErrorDomain - code:iRateErrorApplicationNotFoundOnAppStore - userInfo:@{ - NSLocalizedDescriptionKey : - @"The application could not be found on the App Store." - }]; - } - } else if (!_appStoreID && self.verboseLogging) { - NSLog(@"iRate could not find your app on iTunes. If your app is not yet on the store or is not " - @"intended for App Store release then don't worry about this"); - } - } - } else if (statusCode >= 400) { - // http error - NSString *message = [NSString stringWithFormat:@"The server returned a %@ error", @(statusCode)]; - error = [NSError errorWithDomain:@"HTTPResponseErrorDomain" - code:statusCode - userInfo:@{NSLocalizedDescriptionKey : message}]; - } - - // handle errors (ignoring sandbox issues) - if (error && !(error.code == EPERM && [error.domain isEqualToString:NSPOSIXErrorDomain] && _appStoreID)) { - [self performSelectorOnMainThread:@selector(connectionError:) withObject:error waitUntilDone:YES]; - } else if (self.appStoreID || self.previewMode) { - // show prompt - [self performSelectorOnMainThread:@selector(connectionSucceeded) withObject:nil waitUntilDone:YES]; - } - - // finished - checking = NO; - } -} - -- (void)promptIfNetworkAvailable { - if (!self.checkingForPrompt && !self.checkingForAppStoreID) { - self.checkingForPrompt = YES; - [self checkForConnectivityInBackground]; - } -} - -- (BOOL)promptIfAllCriteriaMet { - if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) { - return NO; - } - if ([self shouldPromptForRating]) { - [self promptIfNetworkAvailable]; - return YES; - } - return NO; -} - -- (BOOL)showRemindButton { - return [self.remindButtonLabel length]; -} - -- (BOOL)showCancelButton { - return [self.cancelButtonLabel length]; -} - -- (void)promptForRating { - if (!self.visibleAlert) { - NSString *message = self.ratedAnyVersion ? self.updateMessage : self.message; - -#if TARGET_OS_IPHONE - - UIViewController *topController = [UIApplication sharedApplication].delegate.window.rootViewController; - while (topController.presentedViewController) { - topController = topController.presentedViewController; - } - - if ([UIAlertController class] && topController && self.useUIAlertControllerIfAvailable) { - UIAlertController *alert = [UIAlertController alertControllerWithTitle:self.messageTitle - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - // rate action - [alert addAction:[UIAlertAction actionWithTitle:self.rateButtonLabel - style:UIAlertActionStyleDefault - handler:^(__unused UIAlertAction *action) { - [self didDismissAlert:alert withButtonAtIndex:0]; - }]]; - - // cancel action - if ([self showCancelButton]) { - [alert addAction:[UIAlertAction actionWithTitle:self.cancelButtonLabel - style:UIAlertActionStyleCancel - handler:^(__unused UIAlertAction *action) { - [self didDismissAlert:alert withButtonAtIndex:1]; - }]]; - } - - // remind action - if ([self showRemindButton]) { - [alert addAction:[UIAlertAction actionWithTitle:self.remindButtonLabel - style:UIAlertActionStyleDefault - handler:^(__unused UIAlertAction *action) { - [self didDismissAlert:alert - withButtonAtIndex:[self showCancelButton] ? 2 : 1]; - }]]; - } - - self.visibleAlert = alert; - - // get current view controller and present alert - [topController presentViewController:alert animated:YES completion:NULL]; - } else { - UIAlertView *alert = [[UIAlertView alloc] initWithTitle:self.messageTitle - message:message - delegate:(id)self - cancelButtonTitle:nil - otherButtonTitles:self.rateButtonLabel, nil]; - if ([self showCancelButton]) { - [alert addButtonWithTitle:self.cancelButtonLabel]; - alert.cancelButtonIndex = 1; - } - - if ([self showRemindButton]) { - [alert addButtonWithTitle:self.remindButtonLabel]; - } - - self.visibleAlert = alert; - [self.visibleAlert show]; - } - -#else - - // only show when main window is available - if (self.onlyPromptIfMainWindowIsAvailable && ![[NSApplication sharedApplication] mainWindow]) { - [self performSelector:@selector(promptForRating) withObject:nil afterDelay:0.5]; - return; - } - - NSAlert *alert = [[NSAlert alloc] init]; - alert.messageText = self.messageTitle; - alert.informativeText = message; - [alert addButtonWithTitle:self.rateButtonLabel]; - if ([self showCancelButton]) { - [alert addButtonWithTitle:self.cancelButtonLabel]; - } - if ([self showRemindButton]) { - [alert addButtonWithTitle:self.remindButtonLabel]; - } - - self.visibleAlert = alert; - -#if __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9 - - if (![alert respondsToSelector:@selector(beginSheetModalForWindow:completionHandler:)]) { - [alert beginSheetModalForWindow:[NSApplication sharedApplication].mainWindow - modalDelegate:self - didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) - contextInfo:nil]; - } else - -#endif - - { - [alert beginSheetModalForWindow:[NSApplication sharedApplication].mainWindow - completionHandler:^(NSModalResponse returnCode) { - [self didDismissAlert:alert withButtonAtIndex:returnCode - NSAlertFirstButtonReturn]; - }]; - } - -#endif - - // inform about prompt - [self.delegate iRateDidPromptForRating]; - [[NSNotificationCenter defaultCenter] postNotificationName:iRateDidPromptForRating object:nil]; - } -} - -- (void)applicationLaunched { - // check if this is a new version - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - NSString *lastUsedVersion = [defaults objectForKey:iRateLastVersionUsedKey]; - if (!self.firstUsed || ![lastUsedVersion isEqualToString:self.applicationVersion]) { - [defaults setObject:self.applicationVersion forKey:iRateLastVersionUsedKey]; - if (!self.firstUsed || [self ratedAnyVersion]) { - // reset defaults - [defaults setObject:[NSDate date] forKey:iRateFirstUsedKey]; - [defaults setInteger:0 forKey:iRateUseCountKey]; - [defaults setInteger:0 forKey:iRateEventCountKey]; - [defaults setObject:nil forKey:iRateLastRemindedKey]; - [self synchronizeWhenSafeToDoSo]; - } else if ([[NSDate date] timeIntervalSinceDate:self.firstUsed] > - (self.daysUntilPrompt - 1) * SECONDS_IN_A_DAY) { - // if was previously installed, but we haven't yet prompted for a rating - // don't reset, but make sure it won't rate for a day at least - self.firstUsed = [[NSDate date] dateByAddingTimeInterval:(self.daysUntilPrompt - 1) * -SECONDS_IN_A_DAY]; - } - - // inform about app update - [self.delegate iRateDidDetectAppUpdate]; - [[NSNotificationCenter defaultCenter] postNotificationName:iRateDidDetectAppUpdate object:nil]; - } - - [self incrementUseCount]; - if (self.shouldPromptForRating) { - [self promptForRating]; - } -} - -- (void)didDismissAlert:(__unused id)alertView withButtonAtIndex:(NSInteger)buttonIndex { - // get button indices - NSInteger rateButtonIndex = 0; - NSInteger cancelButtonIndex = [self showCancelButton] ? 1 : 0; - NSInteger remindButtonIndex = [self showRemindButton] ? cancelButtonIndex + 1 : 0; - - if (buttonIndex == rateButtonIndex) { - [self rate]; - } else if (buttonIndex == cancelButtonIndex) { - [self declineThisVersion]; - } else if (buttonIndex == remindButtonIndex) { - [self remindLater]; - } - - // release alert - self.visibleAlert = nil; -} - -#if TARGET_OS_IPHONE - -- (void)applicationWillEnterForeground { - if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { - [self incrementUseCount]; - } -} - -// Only synchronize when active. -- (void)synchronizeWhenSafeToDoSo -{ - if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) { - self.hasQueuedSynchronize = YES; - return; - } - [[NSUserDefaults standardUserDefaults] synchronize]; - self.hasQueuedSynchronize = NO; -} - -// Only synchronize when active. -- (void)applicationDidBecomeActive -{ - if (self.hasQueuedSynchronize) { - [[NSUserDefaults standardUserDefaults] synchronize]; - self.hasQueuedSynchronize = NO; - } - if (self.promptAtLaunch && !self.hasPromptedAtLaunch) { - if ([self promptIfAllCriteriaMet]) { - self.hasPromptedAtLaunch = YES; - } - } -} - -- (void)openRatingsPageInAppStore { - if (!_ratingsURL && !self.appStoreID) { - self.checkingForAppStoreID = YES; - if (!self.checkingForPrompt) { - [self checkForConnectivityInBackground]; - } - return; - } - - NSString *cantOpenMessage = nil; - -#if TARGET_IPHONE_SIMULATOR - - if ([[self.ratingsURL scheme] isEqualToString:iRateiOSAppStoreURLScheme]) { - cantOpenMessage = - @"iRate could not open the ratings page because the App Store is not available on the iOS simulator"; - } - -#elif DEBUG - - if (![[UIApplication sharedApplication] canOpenURL:self.ratingsURL]) { - cantOpenMessage = - [NSString stringWithFormat:@"iRate was unable to open the specified ratings URL: %@", self.ratingsURL]; - } - -#endif - - if (cantOpenMessage) { - NSLog(@"%@", cantOpenMessage); - NSError *error = [NSError errorWithDomain:iRateErrorDomain - code:iRateErrorCouldNotOpenRatingPageURL - userInfo:@{NSLocalizedDescriptionKey : cantOpenMessage}]; - [self.delegate iRateCouldNotConnectToAppStore:error]; - [[NSNotificationCenter defaultCenter] postNotificationName:iRateCouldNotConnectToAppStore object:error]; - } else { - if (self.verboseLogging) { - NSLog(@"iRate will open the App Store ratings page using the following URL: %@", self.ratingsURL); - } - - [[UIApplication sharedApplication] openURL:self.ratingsURL]; - [self.delegate iRateDidOpenAppStore]; - [[NSNotificationCenter defaultCenter] postNotificationName:iRateDidOpenAppStore object:nil]; - } -} - -- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { - [self didDismissAlert:alertView withButtonAtIndex:buttonIndex]; -} - -#else - -- (void)openAppPageWhenAppStoreLaunched { - // check if app store is running - for (NSRunningApplication *app in [[NSWorkspace sharedWorkspace] runningApplications]) { - if ([app.bundleIdentifier isEqualToString:iRateMacAppStoreBundleID]) { - // open app page - [[NSWorkspace sharedWorkspace] performSelector:@selector(openURL:) - withObject:self.ratingsURL - afterDelay:MAC_APP_STORE_REFRESH_DELAY]; - return; - } - } - - // try again - [self performSelector:@selector(openAppPageWhenAppStoreLaunched) withObject:nil afterDelay:0.0]; -} - -- (void)openRatingsPageInAppStore { - if (!_ratingsURL && !self.appStoreID) { - self.checkingForAppStoreID = YES; - if (!self.checkingForPrompt) { - [self checkForConnectivityInBackground]; - } - return; - } - - if (self.verboseLogging) { - NSLog(@"iRate will open the App Store ratings page using the following URL: %@", self.ratingsURL); - } - - [[NSWorkspace sharedWorkspace] openURL:self.ratingsURL]; - [self openAppPageWhenAppStoreLaunched]; - [self.delegate iRateDidOpenAppStore]; -} - -- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(__unused void *)contextInfo { - [self didDismissAlert:alert withButtonAtIndex:returnCode - NSAlertFirstButtonReturn]; -} - -#endif - -- (void)logEvent:(BOOL)deferPrompt { - [self incrementEventCount]; - if (!deferPrompt) { - [self promptIfAllCriteriaMet]; - } -} - -#pragma mark - User's actions - -- (void)declineThisVersion { - // ignore this version - self.declinedThisVersion = YES; - - // log event - [self.delegate iRateUserDidDeclineToRateApp]; - [[NSNotificationCenter defaultCenter] postNotificationName:iRateUserDidDeclineToRateApp object:nil]; -} - -- (void)remindLater { - // remind later - self.lastReminded = [NSDate date]; - - // log event - [self.delegate iRateUserDidRequestReminderToRateApp]; - [[NSNotificationCenter defaultCenter] postNotificationName:iRateUserDidRequestReminderToRateApp object:nil]; -} - -- (void)rate { - // mark as rated - self.ratedThisVersion = YES; - - // log event - [self.delegate iRateUserDidAttemptToRateApp]; - [[NSNotificationCenter defaultCenter] postNotificationName:iRateUserDidAttemptToRateApp object:nil]; - - if ([self.delegate iRateShouldOpenAppStore]) { - // launch mac app store - [self openRatingsPageInAppStore]; - } -} - -@end diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 087db1f24..687b0aa87 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -3,7 +3,6 @@ // #import "AppDelegate.h" -#import "AppStoreRating.h" #import "AppUpdateNag.h" #import "CodeVerificationViewController.h" #import "DebugLogger.h" @@ -605,7 +604,6 @@ static NSTimeInterval launchStartedAt; [[[OWSFailedAttachmentDownloadsJob alloc] initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]] run]; - [AppStoreRating setupRatingLibrary]; }); } else { DDLogInfo(@"%@ running post launch block for unregistered user.", self.logTag); @@ -917,8 +915,6 @@ static NSTimeInterval launchStartedAt; } DDLogInfo(@"%@ %s %@", self.logTag, __PRETTY_FUNCTION__, notification); - - [AppStoreRating preventPromptAtNextTest]; [AppReadiness runNowOrWhenAppIsReady:^{ [[PushManager sharedManager] application:application didReceiveLocalNotification:notification]; }]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 70b908d55..8b0079e03 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2821,6 +2821,9 @@ typedef enum : NSUInteger { [self clearUnreadMessagesIndicator]; self.inputToolbar.quotedReply = nil; + if (!Environment.preferences.getHasSentAMessage) { + [Environment.preferences setHasSentAMessage:YES]; + } if ([Environment.preferences soundInForeground]) { SystemSoundID soundId = [OWSSounds systemSoundIDForSound:OWSSound_MessageSent quiet:YES]; AudioServicesPlaySystemSound(soundId); diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 535d6cd54..e5c817b16 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -29,6 +29,7 @@ #import #import #import +#import #import #import #import @@ -77,7 +78,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations @property (nonatomic, readonly) NSCache *threadViewModelCache; @property (nonatomic) BOOL isViewVisible; @property (nonatomic) BOOL shouldObserveDBModifications; -@property (nonatomic) BOOL hasBeenPresented; +@property (nonatomic) BOOL hasEverAppeared; // Mark: Search @@ -463,7 +464,11 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations self.hasThemeChanged = NO; } + [self requestReviewIfAppropriate]; + [self.searchResultsController viewDidAppear:animated]; + + self.hasEverAppeared = YES; } - (void)viewDidDisappear:(BOOL)animated @@ -739,13 +744,11 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations ExperienceUpgradesPageViewController *experienceUpgradeViewController = [[ExperienceUpgradesPageViewController alloc] initWithExperienceUpgrades:unseenUpgrades]; [self presentViewController:experienceUpgradeViewController animated:YES completion:nil]; - } else if (!self.hasBeenPresented && [ProfileViewController shouldDisplayProfileViewOnLaunch]) { + } else if (!self.hasEverAppeared && [ProfileViewController shouldDisplayProfileViewOnLaunch]) { [ProfileViewController presentForUpgradeOrNag:self]; } else { [OWSAlerts showIOSUpgradeNagIfNecessary]; } - - self.hasBeenPresented = YES; } - (void)tableViewSetUp @@ -1501,7 +1504,6 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations if (self.homeViewMode == HomeViewMode_Inbox) { if ([Environment.preferences getHasSentAMessage]) { - // FIXME: This doesn't appear to ever show up as the defaults flag is never set (setHasSentAMessage: is never called). firstLine = NSLocalizedString(@"EMPTY_INBOX_FIRST_TITLE", @""); secondLine = NSLocalizedString(@"EMPTY_INBOX_FIRST_TEXT", @""); } else { @@ -1538,6 +1540,25 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations _emptyBoxLabel.attributedText = fullLabelString; } +// We want to delay asking for a review until an opportune time. +// If the user has *just* launched Signal they intend to do something, we don't want to interrupt them. +// If the user hasn't sent a message, we don't want to ask them for a review yet. +- (void)requestReviewIfAppropriate +{ + if (self.hasEverAppeared && Environment.preferences.getHasSentAMessage) { + DDLogDebug(@"%@ in %s requesting review", self.logTag, __PRETTY_FUNCTION__); + if (@available(iOS 10, *)) { + // In Debug this pops up *every* time, which is helpful, but annoying. + // In Production this will pop up at most 3 times per 365 days. +#ifndef DEBUG + [SKStoreReviewController requestReview]; +#endif + } + } else { + DDLogDebug(@"%@ in %s not requesting review", self.logTag, __PRETTY_FUNCTION__); + } +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/AppStoreRating.h b/Signal/src/util/AppStoreRating.h deleted file mode 100644 index 3f08d7fcc..000000000 --- a/Signal/src/util/AppStoreRating.h +++ /dev/null @@ -1,10 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -@interface AppStoreRating : NSObject - -+ (void)setupRatingLibrary; -+ (void)preventPromptAtNextTest; - -@end diff --git a/Signal/src/util/AppStoreRating.m b/Signal/src/util/AppStoreRating.m deleted file mode 100644 index e602831b7..000000000 --- a/Signal/src/util/AppStoreRating.m +++ /dev/null @@ -1,32 +0,0 @@ -// -// AppStoreRating.m -// Signal -// -// Created by Frederic Jacobs on 23/08/15. -// Copyright (c) 2015 Open Whisper Systems. All rights reserved. -// - -#import "AppStoreRating.h" -#import "iRate.h" - -@implementation AppStoreRating - -+ (void)setupRatingLibrary { - iRate *rate = [iRate sharedInstance]; - rate.appStoreID = 874139669; - rate.appStoreGenreID = 6005; - rate.daysUntilPrompt = 15; - rate.usesUntilPrompt = 10; - rate.remindPeriod = 20; - rate.onlyPromptIfLatestVersion = YES; - rate.promptForNewVersionIfUserRated = NO; - rate.messageTitle = NSLocalizedString(@"RATING_TITLE", nil); - rate.message = NSLocalizedString(@"RATING_MSG", nil); - rate.rateButtonLabel = NSLocalizedString(@"RATING_RATE", nil); -} - -+ (void)preventPromptAtNextTest { - iRate *rate = [iRate sharedInstance]; - [rate preventPromptAtNextTest]; -} -@end diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 70d49d827..67b798a6b 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1625,15 +1625,6 @@ /* Indicates this message is a quoted reply to a video file. */ "QUOTED_REPLY_TYPE_VIDEO" = "Video"; -/* No comment provided by engineer. */ -"RATING_MSG" = "If you enjoy using Signal to have private conversations, you can support our project by rating it. It won't take more than a minute, and will help others find some privacy."; - -/* No comment provided by engineer. */ -"RATING_RATE" = "Rate Signal"; - -/* No comment provided by engineer. */ -"RATING_TITLE" = "Support Signal!"; - /* Label for 'I forgot my PIN' link in the 2FA registration view. */ "REGISTER_2FA_FORGOT_PIN" = "I forgot my PIN."; diff --git a/SignalMessaging/Libraries/JSQMessagesViewController/UIColor+JSQMessages.h b/SignalMessaging/Libraries/JSQMessagesViewController/UIColor+JSQMessages.h index 0dc579817..74d639331 100644 --- a/SignalMessaging/Libraries/JSQMessagesViewController/UIColor+JSQMessages.h +++ b/SignalMessaging/Libraries/JSQMessagesViewController/UIColor+JSQMessages.h @@ -53,4 +53,4 @@ */ - (UIColor *)jsq_colorByDarkeningColorWithValue:(CGFloat)value; -@end \ No newline at end of file +@end