├── .github └── workflows │ └── ios.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── DocumentationAssets ├── button-email-state.jpeg └── button-light-style.jpeg ├── Examples ├── Examples.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── GroceryExpress.xcscheme └── GroceryExpress │ ├── AddressSelectionViewController.swift │ ├── AddressUpdateViewController.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon.png │ │ ├── icon_20pt.png │ │ ├── icon_20pt@2x-1.png │ │ ├── icon_20pt@2x.png │ │ ├── icon_20pt@3x.png │ │ ├── icon_29pt.png │ │ ├── icon_29pt@2x-1.png │ │ ├── icon_29pt@2x.png │ │ ├── icon_29pt@3x.png │ │ ├── icon_40pt.png │ │ ├── icon_40pt@2x-1.png │ │ ├── icon_40pt@2x.png │ │ ├── icon_40pt@3x.png │ │ ├── icon_60pt@2x.png │ │ ├── icon_60pt@3x.png │ │ ├── icon_76pt.png │ │ ├── icon_76pt@2x.png │ │ └── icon_83.5@2x.png │ ├── Contents.json │ ├── Icons │ │ ├── Contents.json │ │ ├── cart.imageset │ │ │ ├── Contents.json │ │ │ └── cart.pdf │ │ ├── chevron_right.imageset │ │ │ ├── Contents.json │ │ │ └── chevron_right.pdf │ │ └── menu.imageset │ │ │ ├── Contents.json │ │ │ └── menu.pdf │ └── Mock UI │ │ ├── Contents.json │ │ ├── calendar_connection_image.imageset │ │ ├── Contents.json │ │ ├── Main value prop@1x.png │ │ ├── Main value prop@2x.png │ │ └── Main value prop@3x.png │ │ ├── cart_ui.imageset │ │ ├── Contents.json │ │ ├── Grocery_shopping cart@1x.png │ │ ├── Grocery_shopping cart@2x.png │ │ └── Grocery_shopping cart@3x.png │ │ ├── location_connection_image.imageset │ │ ├── Contents.json │ │ ├── fruit-1509588@1x.jpeg │ │ ├── fruit-1509588@2x.jpeg │ │ └── fruit-1509588@3x.png │ │ └── store_ui.imageset │ │ ├── Contents.json │ │ ├── Grocery flow@1x.png │ │ ├── Grocery flow@2x.png │ │ └── Grocery flow@3x.png │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── ConnectionCredentials.swift │ ├── ConnectionViewController.swift │ ├── DisplayViewController.swift │ ├── Info.plist │ ├── Main.storyboard │ ├── Settings.swift │ ├── SettingsViewController.swift │ ├── TokenRequest.swift │ ├── UIImageView+NetworkLoading.swift │ └── UIView+Helpers.swift ├── IFTTT SDK.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── IFTTTConnectSDK.xcscheme │ └── SDKHostApp.xcscheme ├── IFTTT SDK ├── API.swift ├── ASWebServiceAuthentication.swift ├── AboutViewController.swift ├── Analytics+DataStructures.swift ├── Analytics.swift ├── AnalyticsNetworkController.swift ├── Array+Helpers.swift ├── Assets.swift ├── AuthenticationSessionPresentationContextProvider.swift ├── Bundle+Helpers.swift ├── CLCircularRegion+Parsing.swift ├── Color.swift ├── ConnectButton+AnimationState.swift ├── ConnectButton+CheckmarkView.swift ├── ConnectButton+Constants.swift ├── ConnectButton+Interaction.swift ├── ConnectButton+LabelAnimator.swift ├── ConnectButton+LabelValue.swift ├── ConnectButton+ProgressBar.swift ├── ConnectButton+Service.swift ├── ConnectButton+Style.swift ├── ConnectButton+SwitchControl.swift ├── ConnectButton+Transition.swift ├── ConnectButton.swift ├── ConnectButtonController+Logging.swift ├── ConnectButtonController+Public.swift ├── ConnectButtonController.swift ├── Connection+Location.swift ├── Connection+Parsing.swift ├── Connection+Request.swift ├── Connection+Storage.swift ├── Connection.swift ├── ConnectionActivationFlow.swift ├── ConnectionConfiguration.swift ├── ConnectionCredentialProvider.swift ├── ConnectionDeeplinkAction.swift ├── ConnectionNetworkController.swift ├── ConnectionNetworkError.swift ├── ConnectionRedirectHandler.swift ├── ConnectionVerificationSession.swift ├── ConnectionsMonitor.swift ├── ConnectionsRegistry.swift ├── ConnectionsSynchronizer.swift ├── ConstraintsMaker.swift ├── EmailValidator.swift ├── EventPublisher.swift ├── Font.swift ├── IFTTT_SDK.h ├── ImageCache.swift ├── ImageDownloader.swift ├── ImageViewNetworkController.swift ├── Info.plist ├── JSON.swift ├── JSONNetworkController.swift ├── Keychain.swift ├── LegalTermsText.swift ├── Library.swift ├── Links.swift ├── LocalizedStrings.swift ├── LocationEvent.swift ├── LocationEventReporter.swift ├── LocationEventStore.swift ├── LocationLibrary.swift ├── LocationMonitor.swift ├── LocationService.swift ├── NativeServices.swift ├── Notification+Redirect.swift ├── PassthroughView.swift ├── PermissionsRequestor.swift ├── PillButton.swift ├── PillView.swift ├── ProgressBarController.swift ├── Reachability.swift ├── RegionEvent.swift ├── RegionEventsRegistry.swift ├── RegionsMonitor.swift ├── Resources │ ├── Assets.xcassets │ │ ├── About page │ │ │ ├── Contents.json │ │ │ ├── Value props │ │ │ │ ├── Contents.json │ │ │ │ ├── ifttt_about_connect.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── about_connect.pdf │ │ │ │ ├── ifttt_about_control.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── about_control.pdf │ │ │ │ ├── ifttt_about_security.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── about_security.pdf │ │ │ │ └── ifttt_about_unplug.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── about_unplug.pdf │ │ │ ├── ifttt_about_close.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── about_close.pdf │ │ │ ├── ifttt_about_connect_arrow.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── connect_arrow.pdf │ │ │ └── ifttt_about_download_on_app_store.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── about_download_on_app_store.pdf │ │ ├── Contents.json │ │ └── ifttt_email_confirm.imageset │ │ │ ├── Contents.json │ │ │ └── email_confirm.pdf │ ├── Localizable.strings │ ├── Localizable_cs.strings │ ├── Localizable_da.strings │ ├── Localizable_de.strings │ ├── Localizable_en-GB.strings │ ├── Localizable_en-US.strings │ ├── Localizable_es-419.strings │ ├── Localizable_es.strings │ ├── Localizable_fi.strings │ ├── Localizable_fr-CA.strings │ ├── Localizable_fr.strings │ ├── Localizable_it.strings │ ├── Localizable_ja.strings │ ├── Localizable_ko.strings │ ├── Localizable_nb.strings │ ├── Localizable_nl.strings │ ├── Localizable_pl.strings │ ├── Localizable_pt-BR.strings │ ├── Localizable_pt-PT.strings │ ├── Localizable_ru.strings │ ├── Localizable_sv.strings │ ├── Localizable_zh-Hans.strings │ └── Localizable_zh-Hant.strings ├── Result+Queries.swift ├── SFWebServiceAuthentication.swift ├── SelectGestureRecognizer.swift ├── Selectable.swift ├── ServiceIconsNetworkController.swift ├── Set+Helpers.swift ├── SignInWithAppleAuthentication.swift ├── SynchronizationManager.swift ├── SynchronizationScheduler.swift ├── SynchronizationSubscriber.swift ├── SynchronizationTriggerEvent.swift ├── UIKit+ConvenienceInit.swift ├── URLComponents+email.swift ├── URLRequest+CommonValues.swift ├── URLSession+JSONTask.swift ├── User.swift └── WebServiceAuthentication.swift ├── IFTTTConnectSDK.podspec ├── LICENSE ├── Location.md ├── Package.swift ├── README.md ├── SDKHostApp ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── ContentView.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SDKHostApp.entitlements └── SceneDelegate.swift ├── SDKHostAppTests ├── ArrayHelpersTests.swift ├── CLRegion+Parsing_spec.swift ├── Connection_ParsingTests.swift ├── ConnectionsRegistryTests.swift ├── EventPublisherTests.swift ├── IFTTT SDKTests-Bridging-Header.h ├── Info.plist ├── LocationEventReporterTests.swift ├── LocationEventStoreTests.swift ├── LocationServiceTests.swift ├── RegionEventsRegistryTests.swift ├── RegionsMonitorTests.swift ├── String_EmailDataDetectorTests.swift ├── SynchronizationSchedulerTests.swift ├── fetch_connection_response.json └── fetch_multiple_feature_fields_connection_response.json └── SDKHostAppUITests ├── Info.plist └── SDKHostAppUITests.swift /.github/workflows/ios.yml: -------------------------------------------------------------------------------- 1 | name: Build and run tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test SDKHostApp scheme using any available iPhone simulator 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Build and Test 18 | env: 19 | scheme: ${{ 'SDKHostApp' }} 20 | run: | 21 | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) 22 | 23 | if [ $scheme = default ]; then scheme=$(cat default); fi 24 | 25 | # Determine file to build: .xcworkspace or .xcodeproj 26 | if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi 27 | 28 | # Clean up whitespace 29 | file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` 30 | 31 | # Find first available simulator 32 | device_name=$(xcrun simctl list devices available | grep "iPhone" | head -n 1 | sed -E 's/^[[:space:]]*([^()]+)[[:space:]]*\(.*$/\1/' | awk '{$1=$1; print}') 33 | 34 | if [ -z "$device_name" ]; then 35 | echo "❌ Failed to find a valid iOS device." 36 | exit 1 37 | fi 38 | 39 | echo "📱 Using device: $device_name" 40 | 41 | # Build and run the tests 42 | xcodebuild test \ 43 | -scheme "$scheme" \ 44 | -"$filetype_parameter" "$file_to_build" \ 45 | -destination "platform=iOS Simulator,name=$device_name" 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | # OSX 6 | .DS_Store 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | # Package.resolved 44 | .build/ 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | # Pods/ 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 67 | 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots/**/*.png 71 | fastlane/test_output 72 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | xcode_project: IFTTT SDK.xcodeproj 3 | xcode_scheme: SDKHostApp 4 | osx_image: xcode12.2 5 | xcode_destination: platform=iOS Simulator,OS=14.2,name=iPhone 11 6 | -------------------------------------------------------------------------------- /DocumentationAssets/button-email-state.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/DocumentationAssets/button-email-state.jpeg -------------------------------------------------------------------------------- /DocumentationAssets/button-light-style.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/DocumentationAssets/button-light-style.jpeg -------------------------------------------------------------------------------- /Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Examples.xcodeproj/xcshareddata/xcschemes/GroceryExpress.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Examples/GroceryExpress/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SDK Example 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import IFTTTConnectSDK 10 | import CoreLocation 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | static let connectionRedirectURL = URL(string: "groceryexpress://connect_callback")! 18 | 19 | private let connectionRedirectHandler = ConnectionRedirectHandler(redirectURL: AppDelegate.connectionRedirectURL) 20 | 21 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 22 | ConnectButtonController.synchronizationLoggingEnabled = true 23 | ConnectButtonController.analyticsEnabled = true 24 | ConnectButtonController.initialize(options: .init(enableSDKBackgroundProcess: true, showPermissionsPrompts: true)) 25 | if ConnectionCredentials(settings: .init()).isLoggedIn { 26 | ConnectButtonController.activate(connections: [DisplayInformation.locationConnection.connectionId]) 27 | } else { 28 | ConnectButtonController.deactivate() 29 | } 30 | 31 | ConnectButtonController.setBackgroundProcessClosures { 32 | print("Background process started!") 33 | } expirationHandler: { 34 | print("Background process expired!") 35 | } 36 | 37 | ConnectButtonController.setLocationEventReportedClosure { events in 38 | print(events) 39 | } 40 | 41 | return true 42 | } 43 | 44 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { 45 | if connectionRedirectHandler.handleApplicationRedirect(url: url, options: options) { 46 | // This is an IFTTT SDK redirect, it will take over from here 47 | return true 48 | } else { 49 | // This is unrelated to the IFTTT SDK 50 | return false 51 | } 52 | } 53 | 54 | func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 55 | ConnectButtonController.performFetchWithCompletionHandler { (result) in 56 | completionHandler(result) 57 | } 58 | } 59 | 60 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 61 | ConnectButtonController.didReceiveSilentRemoteNotification { (result) in 62 | completionHandler(result) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "icon_20pt@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "icon_20pt@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon_29pt@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "icon_29pt@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "icon_40pt@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "icon_40pt@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon_60pt@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon_60pt@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "icon_20pt.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "icon_20pt@2x-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "icon_29pt.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "icon_29pt@2x-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "icon_40pt.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "icon_40pt@2x-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "icon_76pt.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "icon_76pt@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "icon_83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "Icon.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_20pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_20pt.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_29pt.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_40pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_40pt.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_76pt.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Icons/cart.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cart.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Icons/cart.imageset/cart.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Icons/cart.imageset/cart.pdf -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Icons/chevron_right.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "chevron_right.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Icons/chevron_right.imageset/chevron_right.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Icons/chevron_right.imageset/chevron_right.pdf -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Icons/menu.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "menu.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Icons/menu.imageset/menu.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Icons/menu.imageset/menu.pdf -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/calendar_connection_image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Main value prop@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Main value prop@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Main value prop@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/calendar_connection_image.imageset/Main value prop@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/calendar_connection_image.imageset/Main value prop@1x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/calendar_connection_image.imageset/Main value prop@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/calendar_connection_image.imageset/Main value prop@2x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/calendar_connection_image.imageset/Main value prop@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/calendar_connection_image.imageset/Main value prop@3x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/cart_ui.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Grocery_shopping cart@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Grocery_shopping cart@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Grocery_shopping cart@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/cart_ui.imageset/Grocery_shopping cart@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/cart_ui.imageset/Grocery_shopping cart@1x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/cart_ui.imageset/Grocery_shopping cart@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/cart_ui.imageset/Grocery_shopping cart@2x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/cart_ui.imageset/Grocery_shopping cart@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/cart_ui.imageset/Grocery_shopping cart@3x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/location_connection_image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "fruit-1509588@1x.jpeg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "fruit-1509588@2x.jpeg", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "fruit-1509588@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/location_connection_image.imageset/fruit-1509588@1x.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/location_connection_image.imageset/fruit-1509588@1x.jpeg -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/location_connection_image.imageset/fruit-1509588@2x.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/location_connection_image.imageset/fruit-1509588@2x.jpeg -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/location_connection_image.imageset/fruit-1509588@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/location_connection_image.imageset/fruit-1509588@3x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/store_ui.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Grocery flow@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Grocery flow@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Grocery flow@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/store_ui.imageset/Grocery flow@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/store_ui.imageset/Grocery flow@1x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/store_ui.imageset/Grocery flow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/store_ui.imageset/Grocery flow@2x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Assets.xcassets/Mock UI/store_ui.imageset/Grocery flow@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/Examples/GroceryExpress/Assets.xcassets/Mock UI/store_ui.imageset/Grocery flow@3x.png -------------------------------------------------------------------------------- /Examples/GroceryExpress/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /Examples/GroceryExpress/DisplayViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayViewController.swift 3 | // Grocery Express 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | struct DisplayInformation { 12 | let connectionId: String 13 | let hideOpaqueOverlay: Bool 14 | let headerImage: UIImage 15 | let subtitleText: String 16 | let showLocationUpdateWithSkipConfig: Bool 17 | 18 | static let calendarConnection = DisplayInformation(connectionId: "fWj4fxYg", 19 | hideOpaqueOverlay: true, 20 | headerImage: #imageLiteral(resourceName: "calendar_connection_image"), 21 | subtitleText: "Delivered when you're at home", 22 | showLocationUpdateWithSkipConfig: false) 23 | 24 | static let locationConnection = DisplayInformation(connectionId: "pWisyzm7", 25 | hideOpaqueOverlay: false, 26 | headerImage: #imageLiteral(resourceName: "location_connection_image"), 27 | subtitleText: "Delivered when you're at a location", 28 | showLocationUpdateWithSkipConfig: true) 29 | } 30 | 31 | class DisplayViewController: UIViewController { 32 | @IBOutlet weak var calendarConnectionView: UIView! 33 | @IBOutlet weak var locationConnectionView: UIView! 34 | 35 | private let calendarConnectionGestureRecognizer = UITapGestureRecognizer() 36 | private let locationConnectionGestureRecognizer = UITapGestureRecognizer() 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | calendarConnectionView.addGestureRecognizer(calendarConnectionGestureRecognizer) 41 | locationConnectionView.addGestureRecognizer(locationConnectionGestureRecognizer) 42 | 43 | calendarConnectionGestureRecognizer.addTarget(self, action: #selector(calendarConnectionViewTapped)) 44 | locationConnectionGestureRecognizer.addTarget(self, action: #selector(locationConnectionViewTapped)) 45 | } 46 | 47 | @objc func calendarConnectionViewTapped(gestureRecognizer: UIGestureRecognizer) { 48 | navigationController?.pushViewController(ConnectionViewController.instantiate(with: .calendarConnection), animated: true) 49 | } 50 | 51 | @objc func locationConnectionViewTapped(gestureRecognizer: UIGestureRecognizer) { 52 | navigationController?.pushViewController(ConnectionViewController.instantiate(with: .locationConnection), animated: true) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Examples/GroceryExpress/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BGTaskSchedulerPermittedIdentifiers 6 | 7 | com.ifttt.ifttt.synchronization_scheduler 8 | 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | Grocery Express 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleTypeRole 27 | Viewer 28 | CFBundleURLName 29 | com.ifttt.sdk.example 30 | CFBundleURLSchemes 31 | 32 | groceryexpress 33 | 34 | 35 | 36 | CFBundleVersion 37 | $(CURRENT_PROJECT_VERSION) 38 | LSApplicationQueriesSchemes 39 | 40 | ifttt-handoff-v1 41 | message 42 | googlegmail 43 | ms-outlook 44 | ymail 45 | airmail 46 | readdle-spark 47 | 48 | LSRequiresIPhoneOS 49 | 50 | NSLocationAlwaysAndWhenInUseUsageDescription 51 | Grocery Express needs your location to be able to update your Connections. 52 | NSLocationWhenInUseUsageDescription 53 | Grocery Express needs your location to be able to update your Connections. 54 | NSLocationAlwaysUsageDescription 55 | Grocery Express needs your location to be able to update your Connections. 56 | UIBackgroundModes 57 | 58 | location 59 | processing 60 | 61 | UILaunchStoryboardName 62 | LaunchScreen 63 | UIMainStoryboardFile 64 | Main 65 | UIRequiredDeviceCapabilities 66 | 67 | armv7 68 | 69 | UISupportedInterfaceOrientations 70 | 71 | UIInterfaceOrientationPortrait 72 | 73 | UISupportedInterfaceOrientations~ipad 74 | 75 | UIInterfaceOrientationPortrait 76 | UIInterfaceOrientationPortraitUpsideDown 77 | UIInterfaceOrientationLandscapeLeft 78 | UIInterfaceOrientationLandscapeRight 79 | 80 | ITSAppUsesNonExemptEncryption 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Examples/GroceryExpress/Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Settings.swift 3 | // SDK Example 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import IFTTTConnectSDK 10 | 11 | /// Access current app settings 12 | struct Settings { 13 | 14 | /// The current grocery express user email 15 | var email: String 16 | 17 | /// Signals that we should always use the new user connection flow 18 | var forcesNewUserFlow: Bool 19 | 20 | /// Signals that we should always use the connection fetching flow 21 | var fetchConnectionFlow: Bool 22 | 23 | /// Signals that we want to skip the connection configuration in the IFTTT app or the web flow. 24 | var skipConnectionConfiguration: Bool 25 | 26 | /// Determines whether or not geofences are enabled for the location connection 27 | var geofenceEnabled: Bool 28 | 29 | /// The overriding locale to use in updating the Connect Button flow with 30 | var locale: Locale 31 | 32 | /// Gets the current settings 33 | init() { 34 | let defaults = UserDefaults.standard 35 | 36 | let localeId: String 37 | let defaultLocaleId = Locale.current.identifier 38 | 39 | if let settings = defaults.dictionary(forKey: Keys.settings) { 40 | email = settings[Keys.email] as? String ?? "" 41 | forcesNewUserFlow = settings[Keys.forcesNewUserFlow] as? Bool ?? false 42 | fetchConnectionFlow = settings[Keys.fetchConnectionFlow] as? Bool ?? false 43 | skipConnectionConfiguration = settings[Keys.skipConnectionConfiguration] as? Bool ?? false 44 | geofenceEnabled = settings[Keys.geofencesEnabled] as? Bool ?? false 45 | localeId = settings[Keys.localeIdentifier] as? String ?? defaultLocaleId 46 | } else { 47 | email = "" 48 | forcesNewUserFlow = false 49 | fetchConnectionFlow = false 50 | skipConnectionConfiguration = false 51 | geofenceEnabled = false 52 | localeId = defaultLocaleId 53 | } 54 | locale = Locale(identifier: localeId) 55 | } 56 | 57 | /// Saves these settings to disk 58 | func save() { 59 | let settings: [String : Any] = [ 60 | Keys.email : email, 61 | Keys.forcesNewUserFlow : forcesNewUserFlow, 62 | Keys.fetchConnectionFlow : fetchConnectionFlow, 63 | Keys.skipConnectionConfiguration: skipConnectionConfiguration, 64 | Keys.geofencesEnabled: geofenceEnabled, 65 | Keys.localeIdentifier: locale.identifier 66 | ] 67 | UserDefaults.standard.set(settings, forKey: Keys.settings) 68 | } 69 | 70 | private struct Keys { 71 | static let settings = "settings" 72 | static let email = "email" 73 | static let forcesNewUserFlow = "forces_new_user_flow" 74 | static let fetchConnectionFlow = "fetch_connection_flow" 75 | static let skipConnectionConfiguration = "skip_connection_configuration" 76 | static let geofencesEnabled = "geofences_enabled" 77 | static let localeIdentifier = "locale_identifier" 78 | static let isDarkStyle = "is_dark_style" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Examples/GroceryExpress/TokenRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenRequest.swift 3 | // Grocery Express 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Request the IFTTT service token for the current user 11 | /// If the user has already connected Grocery Token, this will grant the service token 12 | struct TokenRequest { 13 | private let credentials: ConnectionCredentials 14 | private let urlRequest: URLRequest 15 | 16 | /// Make the request 17 | /// 18 | /// - Parameter completion: Returns the updated credentials 19 | func start(_ completion: ((ConnectionCredentials) -> Void)? = nil) { 20 | let credentials = self.credentials 21 | let task = URLSession.shared.dataTask(with: urlRequest) { (data, _, _) in 22 | if let data = data, 23 | let response = ((try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any]) as [String : Any]??), 24 | let token = response?["user_token"] as? String { 25 | 26 | credentials.loginUser(with: token) 27 | } 28 | DispatchQueue.main.async { 29 | completion?(credentials) 30 | } 31 | } 32 | task.resume() 33 | } 34 | 35 | /// Creates a `TokenRequest` 36 | /// 37 | /// - Parameter credentials: The credentials used to fetch the token 38 | init(credentials: ConnectionCredentials) { 39 | self.credentials = credentials 40 | self.urlRequest = TokenRequest.tokenURLRequest(for: credentials) 41 | } 42 | 43 | private static func tokenURLRequest(for credentials: ConnectionCredentials) -> URLRequest { 44 | var components = URLComponents(string: "https://grocery-express.ifttt.com/api/user_token")! 45 | components.queryItems = [URLQueryItem(name: "code", value: credentials.oauthCode)] 46 | 47 | // For the Grocery Express service we use the email as the oauth code 48 | // We need to manually encode `+` characters in a user's e-mail because `+` is a valid character that represents a space in a url query. E-mail's with spaces are not valid. 49 | let percentEncodedQuery = components.percentEncodedQuery?.addingPercentEncoding(withAllowedCharacters: .emailEncodingPassthrough) 50 | components.percentEncodedQuery = percentEncodedQuery 51 | 52 | var request = URLRequest(url: components.url!) 53 | request.httpMethod = "POST" 54 | 55 | return request 56 | } 57 | } 58 | 59 | private extension CharacterSet { 60 | 61 | /// This allows '+' character to passthrough for sending an email address as a url parameter. 62 | static let emailEncodingPassthrough = CharacterSet(charactersIn: "+").inverted 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Examples/GroceryExpress/UIView+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Helpers.swift 3 | // Grocery Express 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIView { 12 | func fillConstraint(view: UIView, attribute: NSLayoutConstraint.Attribute, constant: CGFloat = 0.0) -> NSLayoutConstraint { 13 | return .init(item: view, 14 | attribute: attribute, 15 | relatedBy: .equal, 16 | toItem: self, 17 | attribute: attribute, 18 | multiplier: 1.0, 19 | constant: constant) 20 | } 21 | 22 | func fillConstraints(view: UIView, constant: CGFloat = 0.0) -> [NSLayoutConstraint] { 23 | let attributes: [NSLayoutConstraint.Attribute] = [.leading, .top, .bottom, .trailing] 24 | return attributes.map { 25 | .init(item: view, 26 | attribute: $0, 27 | relatedBy: .equal, 28 | toItem: self, 29 | attribute: $0, 30 | multiplier: 1.0, 31 | constant: constant) 32 | } 33 | } 34 | 35 | private func sizeConstraint(attribute: NSLayoutConstraint.Attribute, constant: CGFloat) -> NSLayoutConstraint { 36 | return NSLayoutConstraint(item: self, 37 | attribute: attribute, 38 | relatedBy: .equal, 39 | toItem: nil, 40 | attribute: .notAnAttribute, 41 | multiplier: 1, 42 | constant: constant) 43 | } 44 | 45 | func sizeConstraints(size: CGSize) -> [NSLayoutConstraint] { 46 | return [ 47 | sizeConstraint(attribute: .height, constant: size.height), 48 | sizeConstraint(attribute: .width, constant: size.width) 49 | ] 50 | } 51 | } 52 | 53 | extension UIStackView { 54 | /// Removes all arranged subviews of a UIStackView. 55 | func removeAllArrangedSubviews() { 56 | arrangedSubviews.forEach { 57 | removeArrangedSubview($0) 58 | $0.removeFromSuperview() 59 | } 60 | 61 | layoutIfNeeded() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /IFTTT SDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /IFTTT SDK.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /IFTTT SDK.xcodeproj/xcshareddata/xcschemes/IFTTTConnectSDK.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /IFTTT SDK/ASWebServiceAuthentication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASWebServiceAuthentication.swift 3 | // IFTTTConnectSDK 4 | // 5 | // Copyright © 2021 IFTTT. All rights reserved. 6 | // 7 | 8 | import AuthenticationServices 9 | 10 | /// Wraps an `ASWebAuthenticationSession`. Used to authenticate web services on iOS 12 and up. 11 | @available(iOS 12.0, *) 12 | final class ASWebServiceAuthentication: WebServiceAuthentication { 13 | 14 | /// The backing `ASWebAuthenticationSession`. 15 | private var session: ASWebAuthenticationSession? 16 | 17 | /// We must hold a reference to the session context provider so it's not deallocated. 18 | /// Only used with `ASWebAuthenticationSession` in iOS 13 and up. 19 | private var authenticationSessionContextPresentationProvider: AuthenticationSessionContextPresentationProvider? 20 | 21 | /// Creates an instance of ASWebServiceAuthentication. 22 | /// - Parameters 23 | /// - authenticationSessionContextProvider: An optional instance of `AuthenticationSessionContextPresentationProvider` used in configuring the web service authentication object. Optional for iOS 12 but required for iOS 13 and up. 24 | init(authenticationSessionContextPresentationProvider: AuthenticationSessionContextPresentationProvider?) { 25 | self.authenticationSessionContextPresentationProvider = authenticationSessionContextPresentationProvider 26 | } 27 | 28 | @discardableResult 29 | override func start(with parameters: Parameters, completionHandler: @escaping (Result) -> Void) -> Bool { 30 | let asWebAuthenticationSession = ASWebAuthenticationSession(url: parameters.url, 31 | callbackURLScheme: parameters.callbackURLScheme) { url, error in 32 | guard let url = url else { 33 | guard let error = error as? ASWebAuthenticationSessionError else { 34 | completionHandler(.failure(.unknown)) 35 | return 36 | } 37 | switch error.code { 38 | case .canceledLogin: 39 | completionHandler(.failure(.userCanceled)) 40 | default: 41 | completionHandler(.failure(.unknown)) 42 | } 43 | return 44 | } 45 | completionHandler(.success(url)) 46 | } 47 | 48 | if #available(iOS 13.0, *) { 49 | asWebAuthenticationSession.presentationContextProvider = authenticationSessionContextPresentationProvider 50 | asWebAuthenticationSession.prefersEphemeralWebBrowserSession = parameters.prefersEphemeralWebBrowserSession 51 | } 52 | self.session = asWebAuthenticationSession 53 | return asWebAuthenticationSession.start() 54 | } 55 | 56 | override func cancel() { 57 | session?.cancel() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /IFTTT SDK/Array+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Helpers.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | 11 | extension Array where Element: CLRegion { 12 | /// Determines the regions to return closest to the parameter coordinates. Only returns the top `count` locations. 13 | /// 14 | /// - Parameters: 15 | /// - coordinates: The coordinates of the location to use in determining the regions to be returned. 16 | /// - count: The amount of regions to return. 17 | /// - Returns: The `count` closest regions to the parameter location. 18 | func closestRegions(to coordinates: CLLocationCoordinate2D, count: Int) -> [CLRegion] { 19 | let location = CLLocation(latitude: coordinates.latitude, 20 | longitude: coordinates.longitude) 21 | let regionsClosestToLocation = compactMap { $0 as? CLCircularRegion } 22 | .sorted { (region1, region2) -> Bool in 23 | let region1Location = CLLocation(latitude: region1.center.latitude, longitude: region1.center.longitude) 24 | let region2Location = CLLocation(latitude: region2.center.latitude, longitude: region2.center.longitude) 25 | return location.distance(from: region1Location) < location.distance(from: region2Location) 26 | } 27 | let topClosest = regionsClosestToLocation.keepFirst(count) 28 | return topClosest 29 | } 30 | } 31 | 32 | extension Array { 33 | func keepFirst(_ n: Int) -> Array { 34 | let slice = prefix(n) 35 | return Array(slice) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /IFTTT SDK/Assets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Assets.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | struct Assets { 11 | struct Button { 12 | static let emailConfirm = UIImage.iftttAsset(named: "ifttt_email_confirm") 13 | } 14 | struct About { 15 | static let connectArrow = UIImage.iftttAsset(named: "ifttt_about_connect_arrow") 16 | static let connect = UIImage.iftttAsset(named: "ifttt_about_connect") 17 | static let control = UIImage.iftttAsset(named: "ifttt_about_control") 18 | static let security = UIImage.iftttAsset(named: "ifttt_about_security") 19 | static let manage = UIImage.iftttAsset(named: "ifttt_about_unplug") 20 | static let close = UIImage.iftttAsset(named: "ifttt_about_close") 21 | static let downloadOnAppStore = UIImage.iftttAsset(named: "ifttt_about_download_on_app_store") 22 | } 23 | } 24 | 25 | private extension UIImage { 26 | static func iftttAsset(named: String) -> UIImage? { 27 | return UIImage(named: named, in: Bundle.sdk, compatibleWith: nil) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /IFTTT SDK/AuthenticationSessionPresentationContextProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationSessionContextProvider.swift 3 | // IFTTTConnectSDK 4 | // 5 | // Copyright © 2021 IFTTT. All rights reserved. 6 | // 7 | 8 | import AuthenticationServices 9 | 10 | /// A class that conforms to `ASWebAuthenticationPresentationContextProviding`. 11 | class AuthenticationSessionContextPresentationProvider: NSObject, ASWebAuthenticationPresentationContextProviding, ASAuthorizationControllerPresentationContextProviding { 12 | /// The window context that the presentation of the authentication should take place in. 13 | private let presentationContext: UIWindow 14 | 15 | /// Creates an instance of `AuthenticationSessionContextProvider`. 16 | /// 17 | /// - Parameters: 18 | /// - presentationContext: The `UIWindow` instance to use in conforming to `ASWebAuthenticationPresentationContextProviding`. 19 | init(presentationContext: UIWindow) { 20 | self.presentationContext = presentationContext 21 | super.init() 22 | } 23 | 24 | @available(iOS 12.0, *) 25 | func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { 26 | return presentationContext 27 | } 28 | 29 | @available(iOS 13.0, *) 30 | func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { 31 | return presentationContext 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /IFTTT SDK/Bundle+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Helpers.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | private static let ResourceName = "IFTTTConnectSDK" 12 | private static let LocalizedStringResourceName = "IFTTTConnectSDK-Localizations" 13 | private static let BundleExtensionName = "bundle" 14 | 15 | /** 16 | Defines the bundle for the SDK. The bundle for the app could be different from the bundle for the SDK which means that we might need to use the SDK bundle to get assets and other information. 17 | */ 18 | static var sdk: Bundle { 19 | #if SWIFT_PACKAGE 20 | let connectButtonBundle = Bundle.module 21 | #else 22 | let connectButtonBundle = Bundle(for: ConnectButton.self) 23 | #endif 24 | 25 | guard let urlForBundle = connectButtonBundle.url(forResource: ResourceName, withExtension: BundleExtensionName), 26 | let bundle = Bundle(url: urlForBundle) else { 27 | // If we're unable to generate the bundle indicated by `ResourceName`, fall back to returning the bundle for the `ConnectButton` instead. 28 | return connectButtonBundle 29 | } 30 | 31 | return bundle 32 | } 33 | 34 | /** 35 | Defines the bundle for the localized strings for the SDK. The bundle for the app could be different from the bundle for the SDK which means that we might need to use the localized string bundle to get assets and other information. 36 | */ 37 | static var localizedStrings: Bundle { 38 | #if SWIFT_PACKAGE 39 | let connectButtonBundle = Bundle.module 40 | #else 41 | let connectButtonBundle = Bundle(for: ConnectButton.self) 42 | #endif 43 | 44 | guard let urlForBundle = connectButtonBundle.url(forResource: LocalizedStringResourceName, withExtension: BundleExtensionName), 45 | let bundle = Bundle(url: urlForBundle) else { 46 | // If we're unable to generate the bundle indicated by `ResourceName`, fall back to returning the bundle for the `ConnectButton` instead. 47 | return connectButtonBundle 48 | } 49 | 50 | return bundle 51 | } 52 | 53 | /// Determines whether or not background location is enabled as a capability in the app's info dictionary. 54 | var backgroundLocationEnabled: Bool { 55 | guard let backgroundModes = infoDictionary?["UIBackgroundModes"] as? [String] else { return false } 56 | return backgroundModes.contains("location") 57 | } 58 | 59 | /// Determines whether or not background processing is enabled as a capability in the app's info dictionary. 60 | var backgroundProcessingEnabled: Bool { 61 | guard let backgroundModes = infoDictionary?["UIBackgroundModes"] as? [String] else { return false } 62 | return backgroundModes.contains("processing") 63 | } 64 | 65 | /// Determines whether or not the target's info plist contains the IFTTT background processing task identifier 66 | var containsIFTTTBackgroundProcessingIdentifier: Bool { 67 | guard let backgroundTaskIdentifiers = infoDictionary?["BGTaskSchedulerPermittedIdentifiers"] as? [String] else { return false} 68 | 69 | return backgroundTaskIdentifiers.contains(SynchronizationScheduler.BackgroundProcessIdentifier) 70 | } 71 | 72 | var appName: String? { 73 | return object(forInfoDictionaryKey: "CFBundleName") as? String 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /IFTTT SDK/CLCircularRegion+Parsing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLCircularRegion+Parsing.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | 11 | struct Constants { 12 | static let IFTTTRegionPrefix = "ifttt" 13 | } 14 | 15 | extension CLRegion { 16 | /// Determines whether or not the region is an region associated with an IFTTT connection. 17 | var isIFTTTRegion: Bool { 18 | return identifier.lowercased().starts(with: Constants.IFTTTRegionPrefix) 19 | } 20 | } 21 | 22 | extension CLCircularRegion { 23 | private static let locationManager = CLLocationManager() 24 | 25 | func toUserDefaultsJSON() -> JSON { 26 | return [ 27 | "latitude": center.latitude, 28 | "longitude": center.longitude, 29 | "radius": radius, 30 | "identifier": identifier 31 | ] 32 | } 33 | 34 | convenience init?(parser: Parser) { 35 | guard let latitude = parser["latitude"].double, 36 | let longitude = parser["longitude"].double, 37 | let radius = parser["radius"].double, 38 | let identifier = parser["identifier"].string else { 39 | return nil 40 | } 41 | 42 | let center = CLLocationCoordinate2D(latitude: latitude as CLLocationDegrees, 43 | longitude: longitude as CLLocationDegrees) 44 | 45 | self.init(center: center, 46 | radius: radius as CLLocationDistance, 47 | identifier: identifier) 48 | } 49 | 50 | convenience init?(defaultFieldParser: Parser, triggerId: String) { 51 | let locationParser = defaultFieldParser["default_value"] 52 | self.init(parser: locationParser, triggerId: triggerId) 53 | } 54 | 55 | convenience init?(parser: Parser, triggerId: String) { 56 | guard let latitude = parser["lat"].double, 57 | let longitude = parser["lng"].double, 58 | var radius = parser["radius"].double else { 59 | return nil 60 | } 61 | 62 | radius = min(radius, CLCircularRegion.locationManager.maximumRegionMonitoringDistance) 63 | 64 | let center = CLLocationCoordinate2D(latitude: latitude as CLLocationDegrees, longitude: longitude as CLLocationDegrees) 65 | let identifier = triggerId.addIFTTTPrefix() 66 | 67 | self.init(center: center, 68 | radius: radius as CLLocationDistance, 69 | identifier: identifier) 70 | } 71 | 72 | convenience init?(json: JSON, triggerId: String) { 73 | let parser = Parser(content: json) 74 | let locationParser = parser["value"] 75 | self.init(parser: locationParser, triggerId: triggerId) 76 | } 77 | } 78 | 79 | extension String { 80 | func addIFTTTPrefix() -> String { 81 | return "\(Constants.IFTTTRegionPrefix)_\(self)" 82 | } 83 | 84 | func stripIFTTTPrefix() -> String { 85 | let splitString = split(separator: "_") 86 | if splitString.count == 2 && splitString.first?.lowercased() == "ifttt" { 87 | return String(splitString[1]) 88 | } else { 89 | return self 90 | } 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectButton+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectButton+Constants.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | public typealias VoidClosure = () -> Void 11 | 12 | extension ConnectButton { 13 | 14 | // Layout constants 15 | struct Layout { 16 | static let height: CGFloat = 72 17 | static let maximumWidth = 5.0 * height 18 | static let knobInset: CGFloat = borderWidth 19 | static let knobDiameter = height - 2 * knobInset 20 | static let checkmarkDiameter: CGFloat = 42 21 | static let checkmarkLength: CGFloat = 14 22 | static let serviceIconDiameter = 0.5 * knobDiameter 23 | 24 | /// The thickness of the border around the the connect button. 25 | static let borderWidth: CGFloat = 4 26 | 27 | /// The amount by which the email field is offset from the center 28 | static let emailFieldOffset: CGFloat = 4 29 | static let buttonFooterSpacing: CGFloat = 15 30 | } 31 | 32 | struct Color { 33 | static let blue = UIColor(hex: 0x0099FF) 34 | static let lightGrey = UIColor(hex: 0xCCCCCC) 35 | static let mediumGrey = UIColor(hex: 0x666666) 36 | static let grey = UIColor(hex: 0x414141) 37 | static let border = UIColor(white: 1, alpha: 0.32) 38 | static let almostBlack = UIColor(hex: 0x222222) 39 | 40 | static func dynamicColor(light: UIColor, dark: UIColor) -> UIColor { 41 | if #available(iOS 13, *) { 42 | return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in 43 | if UITraitCollection.userInterfaceStyle == .dark { 44 | return dark 45 | } else { 46 | return light 47 | } 48 | } 49 | } else { 50 | return light 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectButton+LabelValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectButton+LabelValue.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension ConnectButton { 11 | 12 | /// Wraps various ways a `UILabel`'s text can be set. 13 | enum LabelValue: Equatable { 14 | 15 | /// The label has no text. 16 | case none 17 | 18 | /// The text of the label. 19 | case text(String) 20 | 21 | /// The attributed text of the label. 22 | case attributed(NSAttributedString) 23 | 24 | /// The text color of the label. 25 | case textColor(UIColor) 26 | 27 | /// Updates the label with the value of the enum. 28 | /// 29 | /// - Parameter label: The label to update the text on. 30 | func update(label: UILabel) { 31 | switch self { 32 | case .none: 33 | label.text = nil 34 | label.attributedText = nil 35 | case .text(let text): 36 | label.text = text 37 | case .attributed(let text): 38 | label.attributedText = text 39 | case .textColor(let color): 40 | label.textColor = color 41 | } 42 | } 43 | 44 | /// Whether there is text provided. 45 | var isEmpty: Bool { 46 | if case .none = self { 47 | return true 48 | } 49 | return false 50 | } 51 | 52 | static func ==(lhs: LabelValue, rhs: LabelValue) -> Bool { 53 | switch (lhs, rhs) { 54 | case (.none, .none): 55 | return true 56 | case (.text(let lhs), .text(let rhs)): 57 | return lhs == rhs 58 | case (.attributed(let lhs), .attributed(let rhs)): 59 | return lhs == rhs 60 | case (.textColor(let lhs), .textColor(let rhs)): 61 | return lhs == rhs 62 | default: 63 | return false 64 | } 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectButton+ProgressBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectButton+ProgressBar.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension ConnectButton { 11 | 12 | /// A `PassthroughView` subclass that represents the progress bar of the connect button. 13 | final class ProgressBar: PassthroughView { 14 | 15 | /// The value of how much of the progress bar has completed. 16 | var fractionComplete: CGFloat = 0 { 17 | didSet { 18 | update() 19 | } 20 | } 21 | 22 | private let track = UIView() 23 | private let bar = PassthroughView() 24 | 25 | private var defaultColor: UIColor { 26 | return Color.dynamicColor( 27 | light: Color.grey, 28 | dark: Color.lightGrey 29 | ) 30 | } 31 | 32 | /// Configures the progress bar background with the optionally provided `Service`. 33 | /// 34 | /// - Parameter service: An optional `Service` to set the backgrund color to. 35 | func configure(with service: Service?) { 36 | bar.backgroundColor = service?.brandColor.contrasting() ?? defaultColor 37 | } 38 | 39 | private func update() { 40 | bar.transform = CGAffineTransform(translationX: (1 - fractionComplete) * -bounds.width, y: 0) 41 | track.layer.cornerRadius = 0.5 * bounds.height // Progress bar should match rounded corners of connect button 42 | } 43 | 44 | override func layoutSubviews() { 45 | super.layoutSubviews() 46 | update() 47 | } 48 | 49 | /// Creates a `ProgressBar`. 50 | init() { 51 | super.init(frame: .zero) 52 | 53 | // The track ensures that the progress bar stays within its intended bounds 54 | 55 | track.clipsToBounds = true 56 | 57 | addSubview(track) 58 | track.constrain.edges(to: layoutMarginsGuide) 59 | 60 | track.addSubview(bar) 61 | bar.constrain.edges(to: track) 62 | 63 | layoutMargins = .zero 64 | } 65 | 66 | @available(*, unavailable) 67 | required init?(coder aDecoder: NSCoder) { 68 | fatalError("init(coder:) has not been implemented") 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectButton+Service.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectButton+Service.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension ConnectButton { 11 | 12 | /// Wraps various information for configuring the connect button based on the service it is connecting. 13 | struct Service { 14 | 15 | /// An optional icon url to use on the button. 16 | let iconURL: URL? 17 | 18 | /// The color associated with the service. 19 | let brandColor: UIColor 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectButton+Style.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectButton+Style.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension ConnectButton { 11 | 12 | /// Adjusts the button for a white or black background 13 | /// 14 | /// - light: Style the button for a white background 15 | public enum Style { 16 | 17 | /// Style the button for a white background 18 | case light 19 | 20 | /// Style the button for a dark background 21 | case dark 22 | 23 | /// Style the button for with dynamic colors: 24 | /// When the user device is on light mode, the style used is equivalent to `light`. 25 | /// When on dark mode, the style is equivalent to `dark`. 26 | /// 27 | /// On iOS versions before 13.0, this style is the same as `light`. 28 | case dynamic 29 | 30 | struct Font { 31 | static let connect = UIFont(name: "AvenirNext-Bold", size: 22)! 32 | static let email = UIFont(name: "AvenirNext-DemiBold", size: 18)! 33 | } 34 | 35 | /// The color to use for the footer based on the style. 36 | var footerColor: UIColor { 37 | return UIColor(hex: 0x999999) 38 | } 39 | 40 | /// The color to use for the button background based on the style 41 | var buttonBackgroundColor: UIColor { 42 | return colors(light: Color.almostBlack, dark: .white) 43 | } 44 | 45 | /// The color to use for the button text based on the style 46 | var textColor: UIColor { 47 | return colors(light: .white, dark: .black) 48 | } 49 | 50 | private func colors(light: UIColor, dark: UIColor) -> UIColor { 51 | switch self { 52 | case .light: 53 | return light 54 | case .dark: 55 | return dark 56 | case .dynamic: 57 | return Color.dynamicColor(light: light, dark: dark) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectButton+Transition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectButton+Transition.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension ConnectButton { 11 | 12 | /// Groups button State and footer value into a single state transition 13 | struct Transition { 14 | 15 | /// An optional `AnimationState` to transition to. 16 | let state: AnimationState? 17 | 18 | /// An optional `LabelValue` to update the footer to. 19 | let footerValue: LabelValue? 20 | 21 | /// How long the transition should take. 22 | let duration: TimeInterval 23 | 24 | /// Creates a `Transition` for the connect button. 25 | /// 26 | /// - Parameters: 27 | /// - state: An `AnimationState` to transition the connect button to. 28 | /// - duration: How long the transition should take. Defaults to 0.4 seconds. 29 | /// - Returns: A configured `Transition`. 30 | static func buttonState(_ state: AnimationState, duration: TimeInterval = 0.4) -> Transition { 31 | return Transition(state: state, footerValue: nil, duration: duration) 32 | } 33 | 34 | /// Creates a `Transition` for the connect button. 35 | /// 36 | /// - Parameters: 37 | /// - state: An `AnimationState` to transition the connect button to. 38 | /// - footerValue: A `LabelValue` to update the footer to. 39 | /// - duration: How long the transition should take. Defaults to 0.4 seconds. 40 | /// - Returns: A configured `Transition`. 41 | static func buttonState(_ state: AnimationState, footerValue: LabelValue, duration: TimeInterval = 0.4) -> Transition { 42 | return Transition(state: state, footerValue: footerValue, duration: duration) 43 | } 44 | 45 | /// Creates a footer `Transition` for the connect button. 46 | /// 47 | /// - Parameters: 48 | /// - value: A `LabelValue` to update the footer to. 49 | /// - duration: How long the transition should take. Defaults to 0.4 seconds. 50 | /// - Returns: A configured `Transition`. 51 | static func footerValue(_ value: LabelValue, duration: TimeInterval = 0.4) -> Transition { 52 | return Transition(state: nil, footerValue: value, duration: duration) 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectButtonController+Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectButtonController+Logging.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension ConnectButtonController { 11 | static func log(handler: ((String) -> Void)?, 12 | isEnabled: Bool, 13 | event: String, 14 | domain: String) { 15 | guard isEnabled else { return } 16 | if let handler = handler { 17 | handler(event) 18 | } else { 19 | NSLog("[ConnectSDK/\(domain)] \(event)") 20 | } 21 | } 22 | 23 | /// Logs localization events to the console only if `localizationLoggingEnabled` is `true`. 24 | /// 25 | /// - Parameters: 26 | /// - event: A string corresponding to the event to log. 27 | static func localizationLog(_ event: String) { 28 | log(handler: localizationLoggingHandler, 29 | isEnabled: localizationLoggingEnabled, 30 | event: event, 31 | domain: "Localization") 32 | } 33 | 34 | /// Logs synchronization events to the console only if `synchronizationLoggingEnabled` is `true`. 35 | /// 36 | /// - Parameters: 37 | /// - event: A string corresponding to the event to log. 38 | static func synchronizationLog(_ event: String) { 39 | log(handler: synchnronizationLoggingHandler, 40 | isEnabled: synchronizationLoggingEnabled, 41 | event: event, 42 | domain: "Synchronization") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /IFTTT SDK/Connection+Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connection+Location.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | 11 | extension Connection.ConnectionStorage { 12 | var hasLocationTriggers: Bool { 13 | let closure: (Trigger) -> Bool = { 14 | switch $0 { 15 | case .location: 16 | return true 17 | } 18 | } 19 | 20 | return allTriggers.contains(where: closure) || activeUserTriggers.contains(where: closure) 21 | } 22 | var locationRegions: [CLCircularRegion] { 23 | return activeUserTriggers.map { (trigger) -> CLCircularRegion in 24 | switch trigger { 25 | case .location(let region): 26 | return region 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /IFTTT SDK/Connection+Request.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connection+Request.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Connection { 11 | 12 | /// Handles network requests related to the `Connection`. 13 | struct Request { 14 | 15 | /// The HTTP request method options. 16 | enum Method: String { 17 | 18 | /// The HTTP GET method. 19 | case GET = "GET" 20 | 21 | /// The HTTP POST method. 22 | case POST = "POST" 23 | } 24 | 25 | /// The `Request`'s `URLRequest` that task are completed on. 26 | public let urlRequest: URLRequest 27 | 28 | /// A `Request` configured to get a `Connection` with the provided identifier. 29 | /// 30 | /// - Parameters: 31 | /// - id: The identifier of the `Connection`. 32 | /// - credentialProvider: An object that handle providing credentials for a request. 33 | /// - Returns: A `Request` configured to get the `Connection`. 34 | public static func fetchConnection(for id: String, credentialProvider: ConnectionCredentialProvider) -> Request { 35 | return Request(path: "/connections/\(id)", method: .GET, credentialProvider: credentialProvider) 36 | } 37 | 38 | /// A disconnection `Request` for a `Connection` with the provided identifier. 39 | /// 40 | /// - Parameters: 41 | /// - id: The identifier of the `Connection`. 42 | /// - credentialProvider: An object that handle providing credentials for a request. 43 | /// - Returns: A `Request` configured to disconnect the `Connection`. 44 | public static func disconnectConnection(with id: String, credentialProvider: ConnectionCredentialProvider) -> Request { 45 | return Request(path: "/connections/\(id)/disable", method: .POST, credentialProvider: credentialProvider) 46 | } 47 | 48 | private init(path: String, method: Method, credentialProvider: ConnectionCredentialProvider) { 49 | let url = API.base.appendingPathComponent(path) 50 | 51 | var request = URLRequest(url: url) 52 | request.httpMethod = method.rawValue 53 | 54 | if let userToken = credentialProvider.userToken, userToken.isEmpty == false { 55 | request.addIftttServiceToken(userToken) 56 | } 57 | 58 | if let inviteCode = credentialProvider.inviteCode, inviteCode.isEmpty == false { 59 | request.addIftttInviteCode(inviteCode) 60 | } 61 | request.addVersionTracking() 62 | 63 | self.urlRequest = request 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /IFTTT SDK/Connection+Storage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connection+Storage.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Connection { 11 | enum NativeServiceDescription: String, CaseIterable { 12 | case location 13 | } 14 | 15 | struct ConnectionStorage: Hashable { 16 | let id: String 17 | var status: Status 18 | let activeUserTriggers: Set 19 | let allTriggers: Set 20 | 21 | var enabledNativeServiceMap: [NativeServiceDescription: Bool] 22 | 23 | init(id: String, 24 | status: Status, 25 | activeUserTriggers: Set, 26 | allTriggers: Set, 27 | enabledNativeServiceMap: [NativeServiceDescription: Bool]) { 28 | self.id = id 29 | self.status = status 30 | self.activeUserTriggers = activeUserTriggers 31 | self.allTriggers = allTriggers 32 | self.enabledNativeServiceMap = enabledNativeServiceMap 33 | } 34 | 35 | init(id: String, 36 | status: Status, 37 | activeUserTriggers: Set, 38 | allTriggers: Set) { 39 | self.id = id 40 | self.status = status 41 | self.activeUserTriggers = activeUserTriggers 42 | self.allTriggers = allTriggers 43 | self.enabledNativeServiceMap = NativeServiceDescription.allCases.reduce(into: [:], { (dict, description) in 44 | dict[description] = true 45 | }) 46 | } 47 | 48 | func hash(into hasher: inout Hasher) { 49 | hasher.combine(id) 50 | } 51 | 52 | static func == (lhs: Connection.ConnectionStorage, rhs: Connection.ConnectionStorage) -> Bool { 53 | return lhs.id == rhs.id 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectionConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionConfiguration.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Encapsulates the information needed to authenticate a `Connection`'s services. 11 | public struct ConnectionConfiguration { 12 | 13 | /// The identifier for the `Connection`. 14 | public let connectionId: String 15 | 16 | /// The `Connection` for authentication. 17 | public let connection: Connection? 18 | 19 | /// A `String` provided as the suggested user's email address. 20 | public let suggestedUserEmail: String 21 | 22 | /// A `CredentialProvider` conforming object for providing credentials. 23 | public let credentialProvider: ConnectionCredentialProvider 24 | 25 | /// The `URL` that is used for authentication redirects. 26 | public let redirectURL: URL 27 | 28 | /// An flag that allows users to skip configuration of the connection when it's activated either in the IFTTT app or the web flow 29 | public let skipConnectionConfiguration: Bool 30 | 31 | /// Creates a new `ConnectionConfiguration`. 32 | /// 33 | /// - Parameters: 34 | /// - connectionId: The connection identifier to fetch the `Connection` for authentication. 35 | /// - suggestedUserEmail: A `String` with a an email for the user. 36 | /// - credentialProvider: A `CredentialProvider` conforming object for providing credentials. 37 | /// - redirectURL: The `URL` that is used for connection activation redirects. 38 | /// - skipConnectionConfiguration: A `Bool` that is used to skip the configuration of the Connection when it's activated in either the IFTTT app or the web flow. Defaults to `false`. 39 | public init(connectionId: String, 40 | suggestedUserEmail: String, 41 | credentialProvider: ConnectionCredentialProvider, 42 | redirectURL: URL, 43 | skipConnectionConfiguration: Bool = false) { 44 | self.connectionId = connectionId 45 | self.connection = nil 46 | self.suggestedUserEmail = suggestedUserEmail 47 | self.credentialProvider = credentialProvider 48 | self.redirectURL = redirectURL 49 | self.skipConnectionConfiguration = skipConnectionConfiguration 50 | } 51 | 52 | /// Creates a new `ConnectionConfiguration`. 53 | /// 54 | /// - Parameters: 55 | /// - connection: The `Connection` for authentication. 56 | /// - suggestedUserEmail: A `String` with a an email for the user. 57 | /// - credentialProvider: A `CredentialProvider` conforming object for providing credentials. 58 | /// - redirectURL: The `URL` that is used for connection activation redirects. 59 | /// - skipConnectionConfiguration: A `Bool` that is used to skip the configuration of the Connection when it's activated in either the IFTTT app or the web flow. Defaults to `false`. 60 | public init(connection: Connection, 61 | suggestedUserEmail: String, 62 | credentialProvider: ConnectionCredentialProvider, 63 | redirectURL: URL, 64 | skipConnectionConfiguration: Bool = false) { 65 | self.connectionId = connection.id 66 | self.connection = connection 67 | self.suggestedUserEmail = suggestedUserEmail 68 | self.credentialProvider = credentialProvider 69 | self.redirectURL = redirectURL 70 | self.skipConnectionConfiguration = skipConnectionConfiguration 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectionCredentialProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionCredentialProvider.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A protocol that defines APIs for providing credentials used during the service authentication process for an `Connection`. 11 | public protocol ConnectionCredentialProvider { 12 | 13 | /// The OAuth code for your user on your service. This is used to skip a step for connecting to your own service during the Connect Button activation flow. We require this value to provide the best possible user experience. 14 | var oauthCode: String { get } 15 | 16 | /// This is the IFTTT user token for your service. This token allows you to get IFTTT user data related to only your service. For example, include this token to get the enabled status of Connections for your user. It is also the same token that is used to make trigger, query, and action requests for Connections on behave of the user. You should get this token from a communication between your servers and ours using your `IFTTT-Service-Key`. Never include this key in your app binary, rather create on endpoint on your own server to access the user's IFTTT service token. 17 | /// Additionally, the callback from ConnectButtonController returns the user token when a connection is made. 18 | /// You should support both method, in the case that your user has already connected your service to IFTTT. 19 | var userToken: String? { get } 20 | 21 | /// This value is only required if your service is not published. You can find it on https://platform.ifttt.com on the Service tab in General under invite URL. If your service is published, return nil. 22 | var inviteCode: String? { get } 23 | } 24 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectionDeeplinkAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionDeeplinkAction.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | enum ConnectionDeeplinkAction: String { 11 | case view = "view" 12 | case edit = "edit" 13 | case activation = "activation" 14 | 15 | static var isIftttAppAvailable: Bool { 16 | return UIApplication.shared.canOpenURL(URL(string: API.iftttAppScheme)!) 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectionNetworkError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionNetworkError.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An error occurred, preventing the network controller from completing network requests. 11 | public enum NetworkControllerError: Error { 12 | 13 | /// The total retry attempts were exhausted. 14 | case exhaustedRetryAttempts 15 | 16 | /// We got back some invalid response that can't be dealt with 17 | case invalidResponse 18 | 19 | /// Response parameters did not match what we expected. This should never happen. Verify that the latest SDK is being used. 20 | case unknownResponse 21 | 22 | /// The network request got cancelled. This could happen by nil'ing out a network request or by explicitly canceling a network request. 23 | case cancelled 24 | 25 | /// The network request resulted in a authentication error 26 | case authenticationFailure 27 | 28 | /// Could not decode image data. 29 | case invalidImageData 30 | 31 | /// Some generic networking error occurred 32 | case genericError(Error) 33 | 34 | } 35 | 36 | /// An error occurred, preventing the network controller from completing `Connection` network requests. 37 | public enum ConnectionNetworkError: Error { 38 | 39 | /// Some generic networking error occurred 40 | case genericError(Error) 41 | 42 | /// Response parameters did not match what we expected. This should never happen. Verify you are using the latest SDK. 43 | case unknownResponse 44 | 45 | init(networkControllerError: NetworkControllerError) { 46 | switch networkControllerError { 47 | case .genericError(let error): 48 | self = .genericError(error) 49 | default: 50 | self = .unknownResponse 51 | } 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /IFTTT SDK/ConnectionRedirectHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionRedirectHandler.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// A class to handle redirections of `URL`s recieved as a part of the `Connection` activation process. 11 | public final class ConnectionRedirectHandler { 12 | 13 | private let redirectURL: URL 14 | 15 | /// An `AuthenticationRedirectHandler` configured to handle a `URL`. 16 | /// 17 | /// - Parameter redirectURL: A `URL` that is used as the redirect sent on `Connection` activation. 18 | public init(redirectURL: URL) { 19 | self.redirectURL = redirectURL 20 | } 21 | 22 | /// Handles redirects during a `Connection` activation. 23 | /// 24 | /// Generally, this is used to handle url redirects the app recieves in `func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool` in the `AppDelgate`. 25 | /// - Example: `AuthenticationRedirectHandler.handleApplicationRedirect(url: url, options: options)`. 26 | /// 27 | /// - Parameters: 28 | /// - url: The `URL` resource to open. 29 | /// - options: A dictionary of `URL` handling options. For information about the possible keys in this dictionary, see UIApplicationOpenURLOptionsKey. 30 | /// - Returns: True if this is an IFTTT SDK redirect. False for any other `URL`. 31 | public func handleApplicationRedirect(url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool { 32 | 33 | // Checks if the scheme matches the SDK redirect. 34 | if url.scheme == redirectURL.scheme { 35 | NotificationCenter.default.post(name: .connectionRedirect, object: url) 36 | return true 37 | } 38 | 39 | return false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /IFTTT SDK/EmailValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmailValidator.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Determines the validity of an email address. 11 | struct EmailValidator { 12 | 13 | /// Validates the parameter string to make sure it's a valid email address. 14 | /// 15 | /// - Parameters: 16 | /// - text: The email address to validate. 17 | /// - Returns: A boolean value that determines whether or not the parameter text is a valid email address or not. 18 | func validate(with text: String) -> Bool { 19 | // An empty string is not a valid email 20 | if text.isEmpty { return false } 21 | 22 | // Use NSDataDetector to determine whether or not the text is valid 23 | guard let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { 24 | return false 25 | } 26 | 27 | // Remove any spaces in the text 28 | let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines) 29 | let range = NSMakeRange(0, NSString(string: trimmedText).length) 30 | let allMatches = dataDetector.matches(in: trimmedText, 31 | options: [], 32 | range: range) 33 | 34 | if allMatches.count == 1, allMatches.first?.url?.absoluteString.contains("mailto:") == true { 35 | return true 36 | } 37 | return false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /IFTTT SDK/EventPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventPublisher.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Defines an generic threadsafe publisher-subscriber construct allowing multiple subscribers to be 11 | /// notified of events from the publisher. 12 | final class EventPublisher { 13 | /// A typealias to refer to the closure that's executed on each subscriber. 14 | typealias SubscriberClosure = (Type) -> Void 15 | 16 | /// A typealias to refer to a subscriber. 17 | private typealias Subscriber = (TimeInterval, SubscriberClosure) 18 | 19 | /// The `DispatchQueue` used to access the subscriberMap on. 20 | private let subscriberMapDispatchQueue: DispatchQueue 21 | 22 | /// The `DispatchQueue` used to publish events on 23 | private let publisherDispatchQueue: DispatchQueue 24 | 25 | // Don't access this Dictionary directly outside of `addSubscriber(...)` or `removeSubscriber(...)`. Maps aren't threadsafe and therefore we can't use the subscripting directly. 26 | private var subscriberMap = [UUID: Subscriber]() 27 | 28 | /// Creates an instance of `EventPublisher`. 29 | /// 30 | /// - Parameters: 31 | /// - queue: The queue to use in notifying subscribers of any published events. 32 | init(subscriberMapDispatchQueue: DispatchQueue = DispatchQueue(label: "com.ifttt.subscriber_map.lock"), 33 | publisherDispatchQueue: DispatchQueue = DispatchQueue(label: "com.ifttt.publisher", attributes: .concurrent)) { 34 | self.subscriberMapDispatchQueue = subscriberMapDispatchQueue 35 | self.publisherDispatchQueue = publisherDispatchQueue 36 | } 37 | 38 | /// Publishes an event to subscribers. Subscribers get notified of events in descending order of time they were added. 39 | /// 40 | /// - Parameter: 41 | /// - object: The event that is to be delivered to subscribers. 42 | func onNext(_ object: Type) { 43 | subscriberMapDispatchQueue.sync { [weak self] in 44 | /// Sort the subscribers by time they were added. This means that the oldest subscriber gets notified of an event first. 45 | self?.subscriberMap.values.sorted { $0.0 < $1.0 } 46 | .map { $1 } 47 | .forEach { closure in 48 | self?.publisherDispatchQueue.async { 49 | closure(object) 50 | } 51 | } 52 | } 53 | } 54 | 55 | /// Adds a subscriber to be notified of events. 56 | /// 57 | /// - Parameters: 58 | /// - subscriber: A closure that gets called when there's a new published event. 59 | /// - Returns: A `UUID` token that can be used to remove a subscriber using `removeSubscriber`. 60 | @discardableResult 61 | func addSubscriber(_ subscriber: @escaping SubscriberClosure) -> UUID { 62 | return subscriberMapDispatchQueue.sync { [weak self] in 63 | let id = UUID() 64 | self?.subscriberMap[id] = (Date().timeIntervalSince1970, subscriber) 65 | return id 66 | } 67 | } 68 | 69 | /// Removes a subscriber from the subscriber map. 70 | /// 71 | /// - Parameters: 72 | /// - identifier: The token corresponding to the subscriber that is to be removed. 73 | func removeSubscriber(_ identifier: UUID) { 74 | subscriberMapDispatchQueue.sync { [weak self] in 75 | self?.subscriberMap[identifier] = nil 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /IFTTT SDK/IFTTT_SDK.h: -------------------------------------------------------------------------------- 1 | // 2 | // IFTTT_SDK.h 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for IFTTT_SDK. 11 | FOUNDATION_EXPORT double IFTTT_SDKVersionNumber; 12 | 13 | //! Project version string for IFTTT_SDK. 14 | FOUNDATION_EXPORT const unsigned char IFTTT_SDKVersionString[]; 15 | 16 | // All swift so nothing to see here ✌️ 17 | -------------------------------------------------------------------------------- /IFTTT SDK/ImageCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCache.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | struct ImageCache { 11 | 12 | struct Capacity { 13 | private static let MB = 1024 * 1024 // One MegaByte 14 | 15 | static let inMemory = 4 * MB 16 | static let onDisk = 20 * MB 17 | } 18 | 19 | let urlCache: URLCache 20 | 21 | init(urlCache: URLCache = URLCache(memoryCapacity: Capacity.inMemory, 22 | diskCapacity: Capacity.onDisk, 23 | diskPath: nil)) { 24 | self.urlCache = urlCache 25 | } 26 | 27 | func store(imageData: Data, response: URLResponse, for url: URL) { 28 | let cachedResponse = CachedURLResponse(response: response, data: imageData) 29 | urlCache.storeCachedResponse(cachedResponse, for: URLRequest(url: url)) 30 | } 31 | 32 | func image(for url: URL) -> UIImage? { 33 | guard let cachedResponse = urlCache.cachedResponse(for: URLRequest(url: url)) else { 34 | return nil 35 | } 36 | return UIImage(data: cachedResponse.data) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /IFTTT SDK/ImageDownloader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDownloader.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | struct ImageDownloader { 11 | 12 | let urlSession: URLSession 13 | 14 | let cache: ImageCache 15 | 16 | init(urlSession: URLSession = URLSession(configuration: .default), cache: ImageCache = ImageCache()) { 17 | self.urlSession = urlSession 18 | self.cache = cache 19 | } 20 | 21 | @discardableResult 22 | func downloadImage(url: URL, _ completion: @escaping (Result) -> Void) -> URLSessionDataTask? { 23 | if let image = cache.image(for: url) { 24 | completion(.success(image)) 25 | return nil 26 | } 27 | let task = urlSession.dataTask(with: url) { (imageData, response, error) in 28 | if let imageData = imageData, let response = response, let image = UIImage(data: imageData) { 29 | self.cache.store(imageData: imageData, response: response, for: url) 30 | DispatchQueue.main.async { 31 | completion(.success(image)) 32 | } 33 | } else { 34 | DispatchQueue.main.async { 35 | if let networkError = error { 36 | completion(.failure(.genericError(networkError))) 37 | } else { 38 | completion(.failure(.invalidImageData)) 39 | } 40 | } 41 | } 42 | } 43 | task.resume() 44 | return task 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /IFTTT SDK/ImageViewNetworkController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageViewNetworkController.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ImageViewNetworkController { 11 | func setImage(with url: URL?, for imageView: UIImageView) 12 | } 13 | -------------------------------------------------------------------------------- /IFTTT SDK/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /IFTTT SDK/LegalTermsText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LegalText.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Factory for building `NSAttributedString` with links for Terms of Use and our Privacy Policy 11 | struct LegalTermsText { 12 | 13 | /// Creates a string with links to IFTTT's terms of service and privacy policy 14 | /// Prefix text is added before the links in the format "[prefix text] Terms_of_Use and Privacy_Policy" 15 | /// 16 | /// - Parameters: 17 | /// - prefix: The prefix before the links 18 | /// - activateLinks: Adds the link attribute, making the links active. Set to false if handling interaction in a custom maner. Default value is true. 19 | /// - Returns: The constructed `NSAttributedString` 20 | static func string(withPrefix prefix: String, activateLinks: Bool = true, attributes: [NSAttributedString.Key : Any]) -> NSAttributedString { 21 | let text = NSMutableAttributedString(string: prefix, attributes: attributes) 22 | 23 | text.addLink(text: "about.legal.link".localized, 24 | to: Links.privacyAndTerms, 25 | activateLinks: activateLinks, 26 | attributes: attributes) 27 | 28 | return NSAttributedString(attributedString: text) 29 | } 30 | } 31 | 32 | private extension NSMutableAttributedString { 33 | 34 | /// Adds a link to a url for a text snippet 35 | /// 36 | /// - Parameters: 37 | /// - text: The text that have a link added to. 38 | /// - activateLinks: Adds the link attribute to the string 39 | /// - url: The URL to link to 40 | func addLink(text: String, to url: URL, activateLinks: Bool, attributes: [NSAttributedString.Key : Any]) { 41 | let range = mutableString.range(of: text) 42 | assert(range.location != NSNotFound, "This should never happen but if it does, the `text` may include some special characters.") 43 | if range.location != NSNotFound { 44 | if activateLinks { 45 | addAttribute(.link, value: url.absoluteString, range: range) 46 | } 47 | addAttributes(attributes, range: range) 48 | addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /IFTTT SDK/Library.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Library.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LibraryAccess: CustomDebugStringConvertible { 11 | case denied, restricted, notDetermined, authorized 12 | 13 | var canBeAuthorized: Bool { 14 | switch self { 15 | case .denied, .restricted: return false 16 | default: return true 17 | } 18 | } 19 | 20 | var debugDescription: String { 21 | switch self { 22 | case .authorized: return "Authorized" 23 | case .denied: return "Denied" 24 | case .notDetermined: return "Not determined" 25 | case .restricted: return "Restricted" 26 | } 27 | } 28 | } 29 | 30 | protocol Library { 31 | /// Maps the access authorization status of this native library to the `LibraryAccess` generalized type 32 | var access: LibraryAccess { get } 33 | 34 | /// Checks current access to this native library. If it's `notDetermined` then it requests access immediately. 35 | /// 36 | /// - Parameter completion: This will be called immediately if `access != .notDetermined` else it will prompt the user for access. 37 | func requestAccess(_ completion: @escaping (LibraryAccess) -> Void) 38 | } 39 | -------------------------------------------------------------------------------- /IFTTT SDK/Links.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Links.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Defines links to IFTTT properties 11 | struct Links { 12 | 13 | /// The IFTTT privacy & terms of use page 14 | static let privacyAndTerms = URL(string: "https://ifttt.com/terms")! 15 | } 16 | -------------------------------------------------------------------------------- /IFTTT SDK/LocalizedStrings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedStrings.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | func localized(with arguments: CVarArg ...) -> String { 12 | return String(format: localized, locale: nil, arguments: arguments) 13 | } 14 | var localized: String { 15 | // Use the ConnectButtonController's locale to change the string that's returned 16 | let locale = ConnectButtonController.locale 17 | let bundle = Bundle.localizedStrings 18 | let localeIdentifier = locale.identifier 19 | 20 | // String files are suffixed with the locale identifier. Try to grab the strings file and check to see if it exists. 21 | let table = "Localizable_\(localeIdentifier)" 22 | 23 | if let tablePath = bundle.path(forResource: table, ofType: "strings"), 24 | FileManager.default.fileExists(atPath: tablePath) { 25 | return NSLocalizedString(self, 26 | tableName: table, 27 | bundle: bundle, 28 | value: "", 29 | comment: "") 30 | } 31 | else if let languageCode = locale.languageCode, 32 | let languageTableFallbackPath = bundle.path(forResource: "Localizable_\(languageCode)", ofType: "strings"), 33 | FileManager.default.fileExists(atPath: languageTableFallbackPath) { 34 | ConnectButtonController.localizationLog("The key \(self) wasn't found in a strings file with name \(table) in the the bundle. Will try to fallback to Localizable_\(languageCode).strings file. Try reinstalling the SDK and then perform a clean/rebuild") 35 | return NSLocalizedString(self, tableName: "Localizable_\(languageCode)", bundle: bundle, value: "", comment: "") 36 | } else { 37 | ConnectButtonController.localizationLog("The key \(self) wasn't found in a strings file with name \(table) in the the bundle. Will try to fallback to Localizable.strings file. Try reinstalling the SDK and then perform a clean/rebuild") 38 | return NSLocalizedString(self, bundle: bundle, value: "", comment: "") 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /IFTTT SDK/LocationEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationEvent.swift 3 | // IFTTTConnectSDK 4 | // 5 | // Created by Siddharth Sathyam on 10/25/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Descibes a closure that is invoked whenever the SDK generates a `[LocationEvent]` 11 | public typealias LocationEventsClosure = ([LocationEvent]) -> Void 12 | 13 | /// Describes the kinds of region events that can be reported. 14 | public enum RegionEventKind: String { 15 | /// The user entered the region. 16 | case entry = "entry" 17 | 18 | /// The user exited the region. 19 | case exit = "exit" 20 | } 21 | 22 | /// Describes the reasons why an event was not uploaded. 23 | public enum EventUploadError: Error { 24 | 25 | /// The total number of events to be uploaded exceeds a sanity threshold. 26 | case crossedSanityThreshold 27 | 28 | /// A network error ocurred in uploading the event. 29 | case networkError 30 | } 31 | 32 | /// Describes all of the possible events in the Location monitoring flow. 33 | public enum LocationEvent: Equatable { 34 | 35 | /// The location event was recorded by the SDK. 36 | /// 37 | /// - Parameters: 38 | /// - `region`: The details of the region that was recorded. 39 | case reported(region: RegionEvent) 40 | 41 | /// The SDK attempted to upload a region event. 42 | /// 43 | /// - Parameters: 44 | /// - `region`: The details of the region that was attempted to be uploaded. 45 | /// - `delay`: The time in seconds between reporting the event and an attempted upload. 46 | case uploadAttempted(region: RegionEvent, delay: TimeInterval) 47 | 48 | /// The SDK successfully uploaded the region event. 49 | /// 50 | /// - Parameters: 51 | /// - `region`: The details of the region that was successfully uploaded. 52 | /// - `delay`: The time in seconds between reporting the event and a successful upload. 53 | case uploadSuccessful(region: RegionEvent, delay: TimeInterval) 54 | 55 | /// The SDK failed to upload the region event. 56 | /// 57 | /// - Parameters: 58 | /// - `region`: The details of the region that was successfully uploaded. 59 | /// - `error`: The `EventUploadError` that occurred. 60 | /// - `delay`: The time in seconds between attempting the event upload and error in completing the upload. 61 | case uploadFailed(region: RegionEvent, error: EventUploadError, delay: TimeInterval) 62 | } 63 | -------------------------------------------------------------------------------- /IFTTT SDK/LocationLibrary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationLibrarh.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | 11 | final class LocationLibrary: NSObject, Library, CLLocationManagerDelegate { 12 | var access: LibraryAccess { 13 | switch CLLocationManager.authorizationStatus() { 14 | case .authorizedAlways, .authorizedWhenInUse: 15 | return .authorized 16 | case .notDetermined: 17 | return .notDetermined 18 | case .restricted: 19 | return .restricted 20 | case .denied: 21 | return .denied 22 | @unknown default: 23 | return .notDetermined 24 | } 25 | } 26 | 27 | private let locationManager: CLLocationManager 28 | private var completion: ((LibraryAccess) -> Void)? 29 | 30 | init(locationManager: CLLocationManager) { 31 | self.locationManager = locationManager 32 | super.init() 33 | locationManager.delegate = self 34 | } 35 | 36 | func requestAccess(_ completion: @escaping (LibraryAccess) -> Void) { 37 | if access != .authorized { 38 | locationManager.requestAlwaysAuthorization() 39 | } else { 40 | completion(access) 41 | } 42 | } 43 | 44 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 45 | completion?(access) 46 | } 47 | 48 | deinit { 49 | locationManager.delegate = nil 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /IFTTT SDK/LocationMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationMonitor.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | 11 | /// A core protocol to expose CoreLocation's location monitoring. 12 | protocol LocationMonitor { 13 | /// Starts the monitoring 14 | func startMonitor() 15 | 16 | /// Stops the monitoring 17 | func stopMonitor() 18 | 19 | /// Starts or stops the monitoring based on the user's location authorization status. 20 | /// By default, this method starts the monitor if the user has given Always authorization for location and stops it otherwise. 21 | /// 22 | /// - Parameters: 23 | /// - status: The user's permission level for location. 24 | func updateMonitoring(with status: CLAuthorizationStatus) 25 | } 26 | -------------------------------------------------------------------------------- /IFTTT SDK/Notification+Redirect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification.Name+Redirect.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Notification.Name { 11 | 12 | /// A `Notification.Name` used to post notifications when the app recieves a redirect request for a `Connection` activation. 13 | static let connectionRedirect = Notification.Name("ifttt.connection.redirect") 14 | } 15 | -------------------------------------------------------------------------------- /IFTTT SDK/PassthroughView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PassthroughView.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class PassthroughView: UIView { 11 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 12 | let view = super.hitTest(point, with: event) 13 | return view == self ? nil : view 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /IFTTT SDK/PermissionsRequestor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionsRequestor.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import CoreLocation 10 | 11 | /// Handles requesting device permissions 12 | final class PermissionsRequestor { 13 | var showPermissionsPrompts = true 14 | 15 | private let locationManager = CLLocationManager() 16 | 17 | private let queue: OperationQueue = { 18 | let queue = OperationQueue() 19 | queue.maxConcurrentOperationCount = 1 20 | return queue 21 | }() 22 | 23 | private let registry: ConnectionsRegistry 24 | 25 | init(registry: ConnectionsRegistry) { 26 | self.registry = registry 27 | } 28 | 29 | func processUpdate(with connections: Set) { 30 | if !canUpdate() { return } 31 | 32 | let operations = connections.reduce(.init()) { (currSet, connections) -> Set in 33 | return currSet.union(connections.allTriggers) 34 | } 35 | .compactMap { permission -> Library in 36 | switch permission { 37 | case .location: 38 | return LocationLibrary(locationManager: locationManager) 39 | } 40 | }.map { BlockOperation(library: $0) } 41 | 42 | queue.addOperations(operations, waitUntilFinished: false) 43 | } 44 | 45 | private func canUpdate() -> Bool { 46 | return !registry.getConnections().isEmpty 47 | && UserAuthenticatedRequestCredentialProvider.standard.userToken != nil 48 | && UIApplication.shared.applicationState == .active 49 | && showPermissionsPrompts 50 | } 51 | } 52 | 53 | private extension BlockOperation { 54 | convenience init(library: Library) { 55 | self.init { 56 | let semaphore = DispatchSemaphore(value: 0) 57 | DispatchQueue.main.async { 58 | library.requestAccess { _ in 59 | semaphore.signal() 60 | } 61 | } 62 | semaphore.wait() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /IFTTT SDK/PillButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PillButton.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class PillButton: PillView { 11 | 12 | let label = UILabel() 13 | 14 | let imageView = UIImageView() 15 | 16 | func onSelect(_ body: @escaping VoidClosure) { 17 | assert(selectable == nil, "PillButton may have a single select handler") 18 | selectable = Selectable(self, onSelect: body) 19 | selectable?.performHighlight = { [weak self] _, isHighlighted in 20 | self?.label.alpha = isHighlighted ? 0.8 : 1 21 | self?.imageView.alpha = isHighlighted ? 0.8 : 1 22 | self?.isHighlighted = isHighlighted 23 | } 24 | } 25 | 26 | private var selectable: Selectable? 27 | 28 | override var intrinsicContentSize: CGSize { 29 | if imageView.image != nil { 30 | var size = imageView.intrinsicContentSize 31 | size.height += 20 32 | size.width += 20 33 | return size 34 | } else { 35 | return super.intrinsicContentSize 36 | } 37 | } 38 | 39 | init(_ image: UIImage?, _ configure: ((PillButton) -> Void)? = nil) { 40 | super.init() 41 | 42 | addSubview(imageView) 43 | imageView.constrain.center(in: self) 44 | 45 | imageView.image = image 46 | 47 | configure?(self) 48 | } 49 | 50 | init(_ text: String, _ configure: ((PillButton) -> Void)? = nil) { 51 | super.init() 52 | 53 | label.text = text 54 | label.textAlignment = .center 55 | 56 | layoutMargins = UIEdgeInsets(top: 12, left: 32, bottom: 12, right: 32) 57 | addSubview(label) 58 | label.constrain.edges(to: layoutMarginsGuide) 59 | 60 | configure?(self) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /IFTTT SDK/RegionEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegionEvent.swift 3 | // IFTTTConnectSDK 4 | // 5 | // Copyright © 2021 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A record of a region entry/exit event that occurs in the SDK. 11 | public struct RegionEvent: Hashable { 12 | private struct Key { 13 | static let RecordId = "record_id" 14 | static let RegionType = "region_type" 15 | static let OccurredAt = "occurred_at" 16 | static let EventType = "event_type" 17 | static let ChannelId = "channel_id" 18 | static let TriggerSubscriptionId = "trigger_subscription_id" 19 | static let InstallationId = "installation_id" 20 | } 21 | 22 | private struct Constants { 23 | static let Geo = "geo" 24 | static let LocationServiceId = 941030000.description 25 | } 26 | 27 | /// The kind of event this is. 28 | public let kind: RegionEventKind 29 | 30 | /// The id of the event. 31 | let recordId: UUID 32 | 33 | /// The timestamp this event occurred at. 34 | let occurredAt: Date 35 | 36 | /// The trigger subscription id that this event corresponds to. 37 | let triggerSubscriptionId: String 38 | 39 | /// Creates an instance of `RegionEvent`. 40 | /// 41 | /// - Parameters: 42 | /// - recordId: The id of the recorded event. 43 | /// - kind: The kind of region event. 44 | /// - ocurredAt: The timestamp this event occurred at. 45 | /// - triggerSubscriptionId: The trigger subscription id that this event corresponds to. 46 | init(recordId: UUID = UUID(), 47 | kind: RegionEventKind, 48 | occurredAt: Date = Date(), 49 | triggerSubscriptionId: String) { 50 | self.recordId = recordId 51 | self.kind = kind 52 | self.occurredAt = occurredAt 53 | self.triggerSubscriptionId = triggerSubscriptionId 54 | } 55 | 56 | /// Creates an optional instance of `RegionEvent`. 57 | /// 58 | /// - Parameters: 59 | /// - json: The `JSON` that should be used in creating the `RegionEvent` 60 | init?(json: JSON) { 61 | let parser = Parser(content: json) 62 | guard let recordId = parser[Key.RecordId].uuid, 63 | let ocurredAtString = parser[Key.OccurredAt].string, 64 | let ocurredAtDate = APIDateFormatter.satellite.date(from: ocurredAtString), 65 | let eventType = parser[Key.EventType].representation(of: RegionEventKind.self), 66 | let triggerSubscriptionId = parser[Key.TriggerSubscriptionId].string else { 67 | return nil 68 | } 69 | self.recordId = recordId 70 | self.occurredAt = ocurredAtDate 71 | self.kind = eventType 72 | self.triggerSubscriptionId = triggerSubscriptionId 73 | } 74 | 75 | func toJSON(stripPrefix: Bool) -> JSON { 76 | return [ 77 | Key.RecordId: recordId.uuidString, 78 | Key.RegionType: Constants.Geo, 79 | Key.OccurredAt: APIDateFormatter.satellite.string(from: occurredAt), 80 | Key.EventType: kind.rawValue, 81 | Key.ChannelId: Constants.LocationServiceId, 82 | Key.TriggerSubscriptionId: stripPrefix ? triggerSubscriptionId.stripIFTTTPrefix(): triggerSubscriptionId, 83 | Key.InstallationId: API.anonymousId 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /IFTTT SDK/RegionEventsRegistry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegionEventsRegistry.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | struct APIDateFormatter { 11 | static let satellite: DateFormatter = { 12 | let f = DateFormatter() 13 | f.locale = Locale(identifier: "en_US_POSIX") 14 | f.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 15 | f.timeZone = TimeZone(secondsFromGMT: 0) 16 | return f 17 | }() 18 | } 19 | 20 | /// Stores region events information to be able to use in synchronizations. 21 | final class RegionEventsRegistry { 22 | /// Gets all the region events stored in the registry. 23 | func getRegionEvents() -> [RegionEvent] { 24 | guard let array = StorageHelpers.regionEvents else { return [] } 25 | return array.compactMap { $0 as? JSON }.compactMap { RegionEvent(json: $0) } 26 | } 27 | 28 | /// Adds a region event to the registry. 29 | /// 30 | /// - Parameters: 31 | /// - event: The event to add to the registry. 32 | func add(_ event: RegionEvent) { 33 | var array = StorageHelpers.regionEvents 34 | 35 | let storage = event.toJSON(stripPrefix: false) 36 | if array != nil { 37 | array?.append(storage) 38 | } else { 39 | array = [storage] 40 | } 41 | 42 | StorageHelpers.regionEvents = array 43 | } 44 | 45 | /// Removes an array of region events from the registry. 46 | /// 47 | /// - Parameters: 48 | /// - events: The events to remove from the registry. 49 | func remove(_ events: [RegionEvent]) { 50 | var array = StorageHelpers.regionEvents 51 | array?.removeAll(where: { (value) -> Bool in 52 | guard let eventJSON = value as? JSON, 53 | let event = RegionEvent(json: eventJSON) else { return false } 54 | return events.contains { 55 | $0.triggerSubscriptionId == event.triggerSubscriptionId && 56 | $0.recordId == event.recordId 57 | } 58 | }) 59 | 60 | StorageHelpers.regionEvents = array 61 | } 62 | 63 | /// Removes all region events from the registry. 64 | func removeAll() { 65 | StorageHelpers.regionEvents = nil 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/Value props/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_connect.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "about_connect.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_connect.imageset/about_connect.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_connect.imageset/about_connect.pdf -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_control.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "about_control.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_control.imageset/about_control.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_control.imageset/about_control.pdf -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_security.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "about_security.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_security.imageset/about_security.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_security.imageset/about_security.pdf -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_unplug.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "about_unplug.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_unplug.imageset/about_unplug.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/IFTTT SDK/Resources/Assets.xcassets/About page/Value props/ifttt_about_unplug.imageset/about_unplug.pdf -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/ifttt_about_close.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "about_close.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/ifttt_about_close.imageset/about_close.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/IFTTT SDK/Resources/Assets.xcassets/About page/ifttt_about_close.imageset/about_close.pdf -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/ifttt_about_connect_arrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "connect_arrow.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/ifttt_about_connect_arrow.imageset/connect_arrow.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/IFTTT SDK/Resources/Assets.xcassets/About page/ifttt_about_connect_arrow.imageset/connect_arrow.pdf -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/ifttt_about_download_on_app_store.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "about_download_on_app_store.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/About page/ifttt_about_download_on_app_store.imageset/about_download_on_app_store.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/IFTTT SDK/Resources/Assets.xcassets/About page/ifttt_about_download_on_app_store.imageset/about_download_on_app_store.pdf -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/ifttt_email_confirm.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "email_confirm.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Assets.xcassets/ifttt_email_confirm.imageset/email_confirm.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IFTTT/ConnectSDK-iOS/bf1b3a707ad18a9a1096e4a4889a00f569176abd/IFTTT SDK/Resources/Assets.xcassets/ifttt_email_confirm.imageset/email_confirm.pdf -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "common.cancel" = "Cancel"; 9 | 10 | "button.state.loading" = "Loading..."; 11 | "button.state.connect" = "Connect %@"; 12 | "button.state.verifying" = "Verifying..."; 13 | "button.state.creating_account" = "Creating account..."; 14 | "button.state.sign_in" = "Going to %@..."; 15 | "button.state.sign_in.brief" = "Sign in"; 16 | "button.state.saving_configuration" = "Saving settings..."; 17 | "button.state.connecting" = "Connecting..."; 18 | "button.state.connected" = "Connected"; 19 | "button.state.disconnect" = "Slide to disconnect"; 20 | "button.state.disconnecting" = "Disconnecting..."; 21 | "button.state.disconnected" = "Disconnected"; 22 | 23 | "button.email.placeholder" = "Your email"; 24 | 25 | "button.footer.works_with" = "WORKS WITH"; 26 | "button.footer.email.invalid" = "Enter valid email"; 27 | "button.footer.loading.failed.prefix" = "Make sure your phone has internet connection."; 28 | "button.footer.loading.failed.postfix" = "Retry"; 29 | "button.footer.email.prefix" = "Secured with IFTTT. Learn More"; 30 | "button.footer.email.postfix" = "Learn more"; 31 | "button.footer.accountCreation" = "New IFTTT account for %@"; 32 | 33 | "about.title_full" = "IFTTT connects %@ to %@"; 34 | "about.connect" = "Unlock new features when your favorite things work together"; 35 | "about.control" = "Control which services access your data and how they use it"; 36 | "about.security" = "Protect and monitor your data"; 37 | "about.manage" = "Unplug any time"; 38 | "about.connection-deep-link" = "Manage"; 39 | "about.legal.full" = "By using this connection, you agree to our Privacy & Terms"; 40 | "about.legal.prefix" = "By using this connection you agree to our "; 41 | "about.legal.link" = "Privacy & Terms"; 42 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_cs.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Načítání…"; 9 | "button.footer.works_with" = "FUNGUJE S"; 10 | "button.state.verifying" = "Ověřování..."; 11 | "button.state.creating_account" = "Vytváření účtu..."; 12 | "button.footer.accountCreation" = "Nový účet IFTTT pro %@"; 13 | "button.state.sign_in" = "Přechod na %@..."; 14 | "button.state.sign_in.brief" = "Přihlásit se"; 15 | "button.state.saving_configuration" = "Ukládání nastavení..."; 16 | "button.state.connected" = "Připojeno"; 17 | "button.state.disconnect" = "Posunutím odpojit"; 18 | "button.state.disconnecting" = "Odpojování..."; 19 | "button.state.disconnected" = "Odpojeno"; 20 | "button.email.placeholder" = "Váš e-mail"; 21 | "button.footer.email.invalid" = "Zadejte platný e-mail"; 22 | "button.footer.loading.failed.postfix" = "Zkusit znovu"; 23 | "button.state.connect" = "Připojit %@"; 24 | "button.state.connecting" = "Připojování"; 25 | "button.footer.email.prefix" = "Zabezpečeno pomocí IFTTT. Další informace"; 26 | "about.title_full" = "IFTTT propojuje %@ s %@"; 27 | "about.connect" = "Získejte nové funkce, kdy budou vaše oblíbené věci spolupracovat"; 28 | "about.control" = "Ovládejte, které služby mají přístup k vašim údajům a jak je využívají"; 29 | "about.security" = "Chraňte a monitorujte své údaje"; 30 | "about.manage" = "Můžete kdykoli odpojit"; 31 | "about.legal.full" = "Využíváním tohoto propojení vyjadřujete souhlas Ochrana soukromí a smluvní podmínky"; 32 | "button.footer.loading.failed.prefix" = "Zkontrolujte, že má váš telefon přístup k internetu"; 33 | "about.connection-deep-link" = "Spravovat"; 34 | "about.legal.link" = "Ochrana soukromí a smluvní podmínky"; 35 | "button.footer.email.postfix" = "Další informace"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_da.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Indlæser..."; 9 | "button.footer.works_with" = "VIRKER MED"; 10 | "button.state.verifying" = "Bekræfter..."; 11 | "button.state.creating_account" = "Opretter konto..."; 12 | "button.footer.accountCreation" = "Ny IFTTT-konto til %@"; 13 | "button.state.sign_in" = "Går til %@..."; 14 | "button.state.sign_in.brief" = "Log på"; 15 | "button.state.saving_configuration" = "Gemmer indstillinger..."; 16 | "button.state.connected" = "Tilsluttede "; 17 | "button.state.disconnect" = "Skub for afbryde forbindelsen"; 18 | "button.state.disconnecting" = "Afbryder forbindelsen..."; 19 | "button.state.disconnected" = "Forbindelsen er afbrudt."; 20 | "button.email.placeholder" = "Din e-mail"; 21 | "button.footer.email.invalid" = "Indtast gyldig e-mail"; 22 | "button.footer.loading.failed.postfix" = "Nyt forsøg"; 23 | "button.state.connect" = "Opret forbindelse til %@"; 24 | "button.state.connecting" = "Tilslutning"; 25 | "button.footer.email.prefix" = "Sikret med IFTTT. Få flere oplysninger"; 26 | "about.title_full" = "IFTTT opretter forbindelse mellem %@ og %@"; 27 | "about.connect" = "Lås op for nye funktioner, når dine foretrukne ting fungerer sammen"; 28 | "about.control" = "Styr, hvilke tjenester der har adgang til dine data, og hvordan de bruger dem"; 29 | "about.security" = "Beskyt og overvåg dine data"; 30 | "about.manage" = "Du kan afbryde forbindelsen når som helst"; 31 | "about.legal.full" = "Ved at bruge denne forbindelse accepterer du vores Fortrolighedspolitik og vilkår."; 32 | "button.footer.loading.failed.prefix" = "Sørg for, at din telefon har internetforbindelse"; 33 | "about.connection-deep-link" = "Administrer"; 34 | "about.legal.link" = "Fortrolighedspolitik og vilkår"; 35 | "button.footer.email.postfix" = "Få flere oplysninger"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_de.strings: -------------------------------------------------------------------------------- 1 | "button.state.loading" = "Laden…"; 2 | "button.footer.works_with" = "FUNKTIONIERT MIT"; 3 | "button.state.verifying" = "Wird bestätigt ..."; 4 | "button.state.creating_account" = "Konto wird erstellt ..."; 5 | "button.footer.accountCreation" = "Neues IFTTT-Konto für %@"; 6 | "button.state.sign_in" = "Navigieren zu %@ ..."; 7 | "button.state.sign_in.brief" = "Anmelden"; 8 | "button.state.saving_configuration" = "Einstellungen werden gespeichert ..."; 9 | "button.state.connected" = "verbunden"; 10 | "button.state.disconnect" = "Zum Trennen wischen"; 11 | "button.state.disconnecting" = "Wird getrennt ..."; 12 | "button.state.disconnected" = "Verbindung getrennt"; 13 | "button.email.placeholder" = "Ihre E-Mail-Adresse"; 14 | "button.footer.email.invalid" = "Geben Sie eine gültige E-Mail-Adresse ein."; 15 | "button.footer.loading.failed.postfix" = "Wiederholen"; 16 | "button.state.connect" = "Verbinden %@"; 17 | "button.state.connecting" = "Wird verbunden"; 18 | "button.footer.email.prefix" = "Mit IFTTT gesichert Weitere Informationen"; 19 | "about.title_full" = "IFTTT verbindet %@ mit %@."; 20 | "about.connect" = "Schalten Sie neue Funktion frei, indem Sie Ihre Lieblingsapps zusammenarbeiten lassen."; 21 | "about.control" = "Steuern Sie, welche Dienste auf Ihre Daten zugreifen und wie sie diese verwenden."; 22 | "about.security" = "Schützen und überwachen Sie Ihre Daten."; 23 | "about.manage" = "Trennen jederzeit möglich"; 24 | "about.legal.full" = "Durch die Nutzung dieser Verbindung stimmen Sie unseren Datenschutzbestimmungen zu."; 25 | "button.footer.loading.failed.prefix" = "Stellen Sie sicher, dass Ihr Telefon über eine Internetverbindung verfügt."; 26 | "about.connection-deep-link" = "Verwalten"; 27 | "about.legal.link" = "Datenschutzbestimmungen"; 28 | "button.footer.email.postfix" = "Weitere Informationen"; 29 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_en-GB.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Loading..."; 9 | "button.footer.works_with" = "WORKS WITH"; 10 | "button.state.verifying" = "Verifying..."; 11 | "button.state.creating_account" = "Creating account..."; 12 | "button.footer.accountCreation" = "New IFTTT account for %@"; 13 | "button.state.sign_in" = "Going to %@..."; 14 | "button.state.sign_in.brief" = "Sign in"; 15 | "button.state.saving_configuration" = "Saving settings..."; 16 | "button.state.connected" = "Connected "; 17 | "button.state.disconnect" = "Slide to disconnect"; 18 | "button.state.disconnecting" = "Disconnecting..."; 19 | "button.state.disconnected" = "Disconnected"; 20 | "button.email.placeholder" = "Your email"; 21 | "button.footer.email.invalid" = "Enter valid email"; 22 | "button.footer.loading.failed.postfix" = "Retry"; 23 | "button.state.connect" = "Connect %@"; 24 | "button.state.connecting" = "Connecting..."; 25 | "button.footer.email.prefix" = "Secured with IFTTT. Learn More"; 26 | "about.title_full" = "IFTTT connects %@ to %@"; 27 | "about.connect" = "Unlock new features when your favourite things work together"; 28 | "about.control" = "Control which services access your data and how they use it"; 29 | "about.security" = "Protect and monitor your data"; 30 | "about.manage" = "Unplug any time"; 31 | "about.legal.full" = "By using this connection, you agree to our Privacy & Terms"; 32 | "button.footer.loading.failed.prefix" = "Make sure your phone has internet connection"; 33 | "about.connection-deep-link" = "Manage"; 34 | "about.legal.link" = "Privacy & Terms"; 35 | "button.footer.email.postfix" = "Learn more"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_en-US.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "common.cancel" = "Cancel"; 9 | 10 | "button.state.loading" = "Loading..."; 11 | "button.state.connect" = "Connect %@"; 12 | "button.state.verifying" = "Verifying..."; 13 | "button.state.creating_account" = "Creating account..."; 14 | "button.state.sign_in" = "Going to %@..."; 15 | "button.state.sign_in.brief" = "Sign in"; 16 | "button.state.saving_configuration" = "Saving settings..."; 17 | "button.state.connecting" = "Connecting..."; 18 | "button.state.connected" = "Connected"; 19 | "button.state.disconnect" = "Slide to disconnect"; 20 | "button.state.disconnecting" = "Disconnecting..."; 21 | "button.state.disconnected" = "Disconnected"; 22 | 23 | "button.email.placeholder" = "Your email"; 24 | 25 | "button.footer.works_with" = "WORKS WITH"; 26 | "button.footer.email.invalid" = "Enter valid email"; 27 | "button.footer.loading.failed.prefix" = "Make sure your phone has internet connection."; 28 | "button.footer.loading.failed.postfix" = "Retry"; 29 | "button.footer.email.prefix" = "Secured with IFTTT. Learn More"; 30 | "button.footer.email.postfix" = "Learn more"; 31 | "button.footer.accountCreation" = "New IFTTT account for %@"; 32 | 33 | "about.title_full" = "IFTTT connects %@ to %@"; 34 | "about.connect" = "Unlock new features when your favorite things work together"; 35 | "about.control" = "Control which services access your data and how they use it"; 36 | "about.security" = "Protect and monitor your data"; 37 | "about.manage" = "Unplug any time"; 38 | "about.connection-deep-link" = "Manage"; 39 | "about.legal.full" = "By using this connection, you agree to our Privacy & Terms"; 40 | "about.legal.prefix" = "By using this connection you agree to our "; 41 | "about.legal.link" = "Privacy & Terms"; 42 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_es-419.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Cargando..."; 9 | "button.footer.works_with" = "FUNCIONA CON"; 10 | "button.state.verifying" = "Verificando..."; 11 | "button.state.creating_account" = "Creando cuenta..."; 12 | "button.footer.accountCreation" = "Nueva cuenta IFTTT para %@"; 13 | "button.state.sign_in" = "Yendo a %@..."; 14 | "button.state.sign_in.brief" = "Iniciar sesión"; 15 | "button.state.saving_configuration" = "Guardando configuración..."; 16 | "button.state.connected" = "Se conectó con"; 17 | "button.state.disconnect" = "Deslice para desconectar"; 18 | "button.state.disconnecting" = "Desconectando..."; 19 | "button.state.disconnected" = "Desconectado"; 20 | "button.email.placeholder" = "Su correo electrónico"; 21 | "button.footer.email.invalid" = "Ingrese un correo electrónico válido"; 22 | "button.footer.loading.failed.postfix" = "Intentar nuevamente"; 23 | "button.state.connect" = "Conectar %@"; 24 | "button.state.connecting" = "Conectando"; 25 | "button.footer.email.prefix" = "Protegido con IFTTT. Más información"; 26 | "about.title_full" = "IFTTT conecta %@ con %@"; 27 | "about.connect" = "Desbloquee nuevas funciones cuando sus cosas favoritas funcionan juntas"; 28 | "about.control" = "Controle qué servicios acceden a sus datos y cómo los usan"; 29 | "about.security" = "Proteja y monitoree sus datos"; 30 | "about.manage" = "Desconéctese en cualquier momento"; 31 | "about.legal.full" = "Al usar esta conexión, acepta nuestros Términos y privacidad"; 32 | "button.footer.loading.failed.prefix" = "Asegúrese de que su teléfono tenga conexión a internet"; 33 | "about.connection-deep-link" = "Administrar"; 34 | "about.legal.link" = "Términos y privacidad"; 35 | "button.footer.email.postfix" = "Más información"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_es.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Cargando..."; 9 | "button.footer.works_with" = "FUNCIONA CON"; 10 | "button.state.verifying" = "Verificando..."; 11 | "button.state.creating_account" = "Creando cuenta..."; 12 | "button.footer.accountCreation" = "Nueva cuenta IFTTT para %@"; 13 | "button.state.sign_in" = "Yendo a %@ ..."; 14 | "button.state.sign_in.brief" = "Iniciar sesión"; 15 | "button.state.saving_configuration" = "Guardando ajustes ..."; 16 | "button.state.connected" = "Conectado"; 17 | "button.state.disconnect" = "Desliza para desconectar"; 18 | "button.state.disconnecting" = "Desconectando..."; 19 | "button.state.disconnected" = "Desconectado"; 20 | "button.email.placeholder" = "Tu correo electrónico"; 21 | "button.footer.email.invalid" = "Introduce un correo electrónico válido"; 22 | "button.footer.loading.failed.postfix" = "Reintentar"; 23 | "button.state.connect" = "Conectar %@"; 24 | "button.state.connecting" = "Conectando"; 25 | "button.footer.email.prefix" = "Asegurado con IFTTT. Más información"; 26 | "about.title_full" = "IFTTT conecta %@ con %@"; 27 | "about.connect" = "Desbloquea nuevas funciones cuando tus dispositivos favoritos trabajen juntos"; 28 | "about.control" = "Controla qué servicios acceden a tus datos y cómo los usan"; 29 | "about.security" = "Protege y supervisa tus datos"; 30 | "about.manage" = "Desconecta en cualquier momento"; 31 | "about.legal.full" = "Al usar esta conexión, aceptas nuestros Privacidad y términos"; 32 | "button.footer.loading.failed.prefix" = "Asegúrate de que tu teléfono tenga conexión a Internet"; 33 | "about.connection-deep-link" = "Gestionar"; 34 | "about.legal.link" = "Privacidad y términos"; 35 | "button.footer.email.postfix" = "Más información"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_fi.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Ladataan..."; 9 | "button.footer.works_with" = "TOIMII SEURAAVIEN KANSSA:"; 10 | "button.state.verifying" = "Vahvistetaan..."; 11 | "button.state.creating_account" = "Tiliä luodaan..."; 12 | "button.footer.accountCreation" = "Uusi IFTTT-tili käyttäjälle %@"; 13 | "button.state.sign_in" = "Siirrytään kohteeseen %@..."; 14 | "button.state.sign_in.brief" = "Kirjaudu"; 15 | "button.state.saving_configuration" = "Asetuksia tallennetaan..."; 16 | "button.state.connected" = "Yhdistetty: "; 17 | "button.state.disconnect" = "Katkaise yhteys liu'uttamalla"; 18 | "button.state.disconnecting" = "Yhteys katkaistaan..."; 19 | "button.state.disconnected" = "Yhteys katkaistu"; 20 | "button.email.placeholder" = "Sähköpostiosoitteesi"; 21 | "button.footer.email.invalid" = "Anna kelvollinen sähköpostiosoite"; 22 | "button.footer.loading.failed.postfix" = "Yritä uudelleen"; 23 | "button.state.connect" = "Yhdistä %@"; 24 | "button.state.connecting" = "Yhdistetään"; 25 | "button.footer.email.prefix" = "IFTTT-suojattu. Lisätietoja"; 26 | "about.title_full" = "IFTTT yhdistää kohteen %@ kohteeseen %@"; 27 | "about.connect" = "Kun lempiasiasi toimivat yhdessä, voit hyödyntää uusia ominaisuuksia"; 28 | "about.control" = "Määritä, mitkä palvelut voivat käyttää tietojasi ja miten"; 29 | "about.security" = "Suojaa ja tarkastele tietojasi"; 30 | "about.manage" = "Poista käytöstä koska tahansa"; 31 | "about.legal.full" = "Käyttämällä tätä yhteyttä hyväksyt Yksityisyys ja ehdot"; 32 | "button.footer.loading.failed.prefix" = "Varmista, että puhelimesi on yhteydessä internetiin"; 33 | "about.connection-deep-link" = "Hallinnoi"; 34 | "about.legal.link" = "Yksityisyys ja ehdot"; 35 | "button.footer.email.postfix" = "Lisätietoja"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_fr-CA.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Chargement..."; 9 | "button.footer.works_with" = "FONCTIONNE AVEC"; 10 | "button.state.verifying" = "Vérification..."; 11 | "button.state.creating_account" = "Création du compte..."; 12 | "button.footer.accountCreation" = "Nouveau compte IFTTT pour %@"; 13 | "button.state.sign_in" = "Aller à %@..."; 14 | "button.state.sign_in.brief" = "Inscrivez-vous"; 15 | "button.state.saving_configuration" = "Enregistrement des paramètres..."; 16 | "button.state.connected" = "Connecté "; 17 | "button.state.disconnect" = "Faites glisser pour vous déconnecter"; 18 | "button.state.disconnecting" = "Déconnexion..."; 19 | "button.state.disconnected" = "Déconnecté"; 20 | "button.email.placeholder" = "Votre courriel"; 21 | "button.footer.email.invalid" = "Saisir une adresse courriel valide"; 22 | "button.footer.loading.failed.postfix" = "Réessayer"; 23 | "button.state.connect" = "Connecter %@"; 24 | "button.state.connecting" = "Connexion"; 25 | "button.footer.email.prefix" = "Sécurisé avec IFTTT. En savoir plus"; 26 | "about.title_full" = "IFTTT connecte %@ à %@"; 27 | "about.connect" = "Déverrouillez de nouvelles fonctionnalités lorsque vos choses préférées fonctionnent ensemble"; 28 | "about.control" = "Contrôlez quels services accèdent à vos données et comment ils les utilisent"; 29 | "about.security" = "Protégez et surveillez vos données"; 30 | "about.manage" = "Débranchez à tout moment"; 31 | "about.legal.full" = "En utilisant cette connexion, vous acceptez nos Confidentialité et modalités"; 32 | "button.footer.loading.failed.prefix" = "Assurez-vous que votre téléphone dispose d'une connexion Internet"; 33 | "about.connection-deep-link" = "Gérer"; 34 | "about.legal.link" = "Confidentialité et modalités"; 35 | "button.footer.email.postfix" = "En savoir plus"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_fr.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Chargement..."; 9 | "button.footer.works_with" = "FONCTIONNE AVEC"; 10 | "button.state.verifying" = "Vérification..."; 11 | "button.state.creating_account" = "Création du compte..."; 12 | "button.footer.accountCreation" = "Nouveau compte IFTTT pour %@"; 13 | "button.state.sign_in" = "Déplacement vers %@..."; 14 | "button.state.sign_in.brief" = "Connexion"; 15 | "button.state.saving_configuration" = "Enregistrement des paramètres..."; 16 | "button.state.connected" = "Connecté"; 17 | "button.state.disconnect" = "Faites glisser pour vous déconnecter"; 18 | "button.state.disconnecting" = "Déconnexion..."; 19 | "button.state.disconnected" = "Déconnecté"; 20 | "button.email.placeholder" = "Votre e-mail"; 21 | "button.footer.email.invalid" = "Entrez une adresse e-mail valide"; 22 | "button.footer.loading.failed.postfix" = "Réessayer"; 23 | "button.state.connect" = "Connecter %@"; 24 | "button.state.connecting" = "Connexion"; 25 | "button.footer.email.prefix" = "Sécurisé avec IFTTT. En savoir plus"; 26 | "about.title_full" = "IFTTT connecte %@ à %@"; 27 | "about.connect" = "Déverrouillez de nouvelles fonctionnalités lorsque vos objets préférés fonctionnent ensemble"; 28 | "about.control" = "Contrôlez quels services accèdent à vos données et comment ils les utilisent"; 29 | "about.security" = "Protégez et surveillez vos données"; 30 | "about.manage" = "Débranchez à tout moment"; 31 | "about.legal.full" = "En utilisant cette connexion, vous acceptez nos Confidentialité et conditions"; 32 | "button.footer.loading.failed.prefix" = "Assurez-vous que votre téléphone dispose d'une connexion Internet"; 33 | "about.connection-deep-link" = "Gérer"; 34 | "about.legal.link" = "Confidentialité et conditions"; 35 | "button.footer.email.postfix" = "En savoir plus"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_it.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Caricamento..."; 9 | "button.footer.works_with" = "FUNZIONA CON"; 10 | "button.state.verifying" = "Verifica in corso..."; 11 | "button.state.creating_account" = "Creazione dell'account..."; 12 | "button.footer.accountCreation" = "Nuovo account IFTTT per %@"; 13 | "button.state.sign_in" = "Spostamento verso %@..."; 14 | "button.state.sign_in.brief" = "Accedi"; 15 | "button.state.saving_configuration" = "Salvataggio delle impostazioni in corso..."; 16 | "button.state.connected" = "Collegato a"; 17 | "button.state.disconnect" = "Scorri per scollegare"; 18 | "button.state.disconnecting" = "Scollegamento in corso..."; 19 | "button.state.disconnected" = "Scollegato"; 20 | "button.email.placeholder" = "La tua email"; 21 | "button.footer.email.invalid" = "Inserisci un indirizzo e-mail valido"; 22 | "button.footer.loading.failed.postfix" = "Riprova"; 23 | "button.state.connect" = "Collega %@"; 24 | "button.state.connecting" = "Connessione"; 25 | "button.footer.email.prefix" = "Protetto con IFTTT. Per saperne di più"; 26 | "about.title_full" = "IFTTT si collega a %@ per %@"; 27 | "about.connect" = "Sblocca nuove funzioni quando i tuoi elementi preferiti funzionano assieme"; 28 | "about.control" = "Controlla quali servizi accedono ai tuoi dati e come li utilizzano"; 29 | "about.security" = "Proteggi e monitora i tuoi dati"; 30 | "about.manage" = "Scollegati quando vuoi"; 31 | "about.legal.full" = "Utilizzando questo collegamento, l'utente accetta la nostra Informativa sulla privacy e termini"; 32 | "button.footer.loading.failed.prefix" = "Assicurati che il tuo telefono sia collegato a Internet"; 33 | "about.connection-deep-link" = "Gestisci"; 34 | "about.legal.link" = "Informativa sulla privacy e termini"; 35 | "button.footer.email.postfix" = "Per saperne di più"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_ja.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "読み込み中…"; 9 | "button.footer.works_with" = "以下と連携"; 10 | "button.state.verifying" = "確認中..."; 11 | "button.state.creating_account" = "アカウントを作成中..."; 12 | "button.footer.accountCreation" = "%@の新規アカウント"; 13 | "button.state.sign_in" = "%@に移動中..."; 14 | "button.state.sign_in.brief" = "サインイン"; 15 | "button.state.saving_configuration" = "設定を保存中..."; 16 | "button.state.connected" = "接続済み"; 17 | "button.state.disconnect" = "スライドして切断"; 18 | "button.state.disconnecting" = "接続解除中..."; 19 | "button.state.disconnected" = "切断済み"; 20 | "button.email.placeholder" = "電子メール"; 21 | "button.footer.email.invalid" = "有効な電子メールの入力"; 22 | "button.footer.loading.failed.postfix" = "再試行"; 23 | "button.state.connect" = "%@に接続"; 24 | "button.state.connecting" = "接続中"; 25 | "button.footer.email.prefix" = "IFTTTで保護 詳細を確認する"; 26 | "about.title_full" = "IFTTTは%@を%@に接続します"; 27 | "about.connect" = "お気に入りのものを連携すると、新しい機能を利用できます"; 28 | "about.control" = "データにアクセスするサービスとその使用方法を制御します"; 29 | "about.security" = "データを保護し、監視します"; 30 | "about.manage" = "いつでも停止可能"; 31 | "about.legal.full" = "この接続を使用すると、プライバシーと利用規約に同意したことになります"; 32 | "button.footer.loading.failed.prefix" = "スマートフォンがインターネットに接続されていることを確認してください"; 33 | "about.connection-deep-link" = "管理"; 34 | "about.legal.link" = "プライバシーと利用規約"; 35 | "button.footer.email.postfix" = "詳細を確認する"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_ko.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "로드 중..."; 9 | "button.footer.works_with" = "함께 사용 가능"; 10 | "button.state.verifying" = "확인 중..."; 11 | "button.state.creating_account" = "계정을 만드는 중..."; 12 | "button.footer.accountCreation" = "%@을(를) 위한 새 IFTTT 계정"; 13 | "button.state.sign_in" = "%@(으)로 이동하는 중..."; 14 | "button.state.sign_in.brief" = "로그인"; 15 | "button.state.saving_configuration" = "설정 저장 중..."; 16 | "button.state.connected" = "연결된 "; 17 | "button.state.disconnect" = "옆으로 밀어 연결 해제"; 18 | "button.state.disconnecting" = "연결을 해제하는 중..."; 19 | "button.state.disconnected" = "연결 해제됨"; 20 | "button.email.placeholder" = "사용자 이메일"; 21 | "button.footer.email.invalid" = "올바른 이메일 입력"; 22 | "button.footer.loading.failed.postfix" = "재시도"; 23 | "button.state.connect" = "%@에 연결"; 24 | "button.state.connecting" = "연결 중"; 25 | "button.footer.email.prefix" = "IFTTT로 보안 처리되었습니다. 자세히 알아보기"; 26 | "about.title_full" = "IFTTT는 %@을(를) %@에 연결합니다."; 27 | "about.connect" = "즐겨 사용하는 장치를 함께 사용할 때 새 기능 잠금 해제"; 28 | "about.control" = "데이터에 액세스하는 서비스와 사용하는 방법 제어"; 29 | "about.security" = "데이터 보호 및 모니터링"; 30 | "about.manage" = "언제든지 플러그 분리"; 31 | "about.legal.full" = "이 연결 사용 시 당사의 개인정보 개인정보 및 약관"; 32 | "button.footer.loading.failed.prefix" = "전화가 인터넷에 연결되었는지 확인"; 33 | "about.connection-deep-link" = "관리"; 34 | "about.legal.link" = "개인정보 및 약관"; 35 | "button.footer.email.postfix" = "자세히 알아보기"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_nb.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Laster inn ..."; 9 | "button.footer.works_with" = "FUNGERER MED"; 10 | "button.state.verifying" = "Kontrollerer ..."; 11 | "button.state.creating_account" = "Oppretter konto ..."; 12 | "button.footer.accountCreation" = "Ny IFTTT-konto for %@"; 13 | "button.state.sign_in" = "Går til %@ ..."; 14 | "button.state.sign_in.brief" = "Logg på"; 15 | "button.state.saving_configuration" = "Lagrer innstillinger ..."; 16 | "button.state.connected" = "Tilkoblet"; 17 | "button.state.disconnect" = "Skyv for å koble fra"; 18 | "button.state.disconnecting" = "Kobler fra ..."; 19 | "button.state.disconnected" = "Frakoblet"; 20 | "button.email.placeholder" = "E-postadressen din"; 21 | "button.footer.email.invalid" = "Angi en gyldig e-postadresse"; 22 | "button.footer.loading.failed.postfix" = "Prøv igjen"; 23 | "button.state.connect" = "Koble til %@"; 24 | "button.state.connecting" = "Kobler til"; 25 | "button.footer.email.prefix" = "Sikret med IFTTT. Finn ut mer"; 26 | "about.title_full" = "IFTTT kobler %@ til %@"; 27 | "about.connect" = "Lås opp nye funksjoner når favorittingene dine samarbeider"; 28 | "about.control" = "Kontroller hvilke tjenester som har tilgang til dataene dine og hvordan de bruker dem"; 29 | "about.security" = "Beskytt og overvåk dataene dine"; 30 | "about.manage" = "Koble ut når som helst"; 31 | "about.legal.full" = "Ved å bruke denne koblingen, godtar du våre retningslinjer for Personvern og vilkår"; 32 | "button.footer.loading.failed.prefix" = "Kontroller at telefonen har tilkobling til Internett"; 33 | "about.connection-deep-link" = "Administrer"; 34 | "about.legal.link" = "Personvern og vilkår"; 35 | "button.footer.email.postfix" = "Finn ut mer"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_nl.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Bezig met laden..."; 9 | "button.footer.works_with" = "WERKT MET"; 10 | "button.state.verifying" = "Bezig met controleren..."; 11 | "button.state.creating_account" = "Account maken..."; 12 | "button.footer.accountCreation" = "Nieuw IFTTT-account voor %@"; 13 | "button.state.sign_in" = "Gaat %@ naar..."; 14 | "button.state.sign_in.brief" = "Aanmelden"; 15 | "button.state.saving_configuration" = "Instellingen opslaan..."; 16 | "button.state.connected" = "Verbonden"; 17 | "button.state.disconnect" = "Schuiven om verbinding te verbreken"; 18 | "button.state.disconnecting" = "Verbinding verbreken..."; 19 | "button.state.disconnected" = "Verbinding verbroken"; 20 | "button.email.placeholder" = "Uw e-mail"; 21 | "button.footer.email.invalid" = "Vul een geldig e-mailadres in"; 22 | "button.footer.loading.failed.postfix" = "Opnieuw proberen"; 23 | "button.state.connect" = "Verbinden met %@"; 24 | "button.state.connecting" = "Verbinding maken"; 25 | "button.footer.email.prefix" = "Beveiligd met IFTTT. Meer informatie"; 26 | "about.title_full" = "IFTTT verbindt %@ met %@"; 27 | "about.connect" = "Ontgrendel nieuwe functies wanneer uw favoriete dingen samenwerken"; 28 | "about.control" = "Bepaal welke services toegang hebben tot uw gegevens en hoe ze deze gebruiken"; 29 | "about.security" = "Uw gegevens beschermen en controleren"; 30 | "about.manage" = "Op elk gewenst moment loskoppelen"; 31 | "about.legal.full" = "Door deze verbinding te gebruiken, gaat u akkoord met onze Privacy en voorwaarden"; 32 | "button.footer.loading.failed.prefix" = "Zorg dat uw telefoon een internetverbinding heeft"; 33 | "about.connection-deep-link" = "Beheren"; 34 | "about.legal.link" = "Privacy en voorwaarden"; 35 | "button.footer.email.postfix" = "Meer informatie"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_pl.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Wczytywanie..."; 9 | "button.footer.works_with" = "DZIAŁA Z"; 10 | "button.state.verifying" = "Weryfikacja..."; 11 | "button.state.creating_account" = "Tworzenie konta..."; 12 | "button.footer.accountCreation" = "Nowe konto IFTTT dla %@"; 13 | "button.state.sign_in" = "Przechodzenie do %@..."; 14 | "button.state.sign_in.brief" = "Zaloguj się"; 15 | "button.state.saving_configuration" = "Zapisywanie ustawień..."; 16 | "button.state.connected" = "Połączony "; 17 | "button.state.disconnect" = "Przesuń, aby rozłączyć"; 18 | "button.state.disconnecting" = "Rozłączanie..."; 19 | "button.state.disconnected" = "Rozłączono"; 20 | "button.email.placeholder" = "Twój e-mail"; 21 | "button.footer.email.invalid" = "Wprowadź prawidłowy e-mail"; 22 | "button.footer.loading.failed.postfix" = "Spróbuj ponownie"; 23 | "button.state.connect" = "Połącz %@"; 24 | "button.state.connecting" = "Łączenie"; 25 | "button.footer.email.prefix" = "Zabezpieczone za pomocą IFTTT. Dowiedz się więcej"; 26 | "about.title_full" = "IFTTT łączy %@ z %@"; 27 | "about.connect" = "Odkrywaj nowe funkcje, gdy ulubione elementy współpracują ze sobą"; 28 | "about.control" = "Kontroluj, które usługi mogą mieć dostęp do Twoich danych, i jak mogą je wykorzystywać"; 29 | "about.security" = "Monitoruj i chroń swoje dane"; 30 | "about.manage" = "Możesz odłączyć w dowolnym momencie"; 31 | "about.legal.full" = "Korzystanie z tego połączenia jest równoznaczne z wyrażeniem zgody na Warunki i Polityka prywatności"; 32 | "button.footer.loading.failed.prefix" = "Upewnij się, że Twój telefon ma połączenie z Internetem"; 33 | "about.connection-deep-link" = "Zarządzaj"; 34 | "about.legal.link" = "Warunki i Polityka prywatności"; 35 | "button.footer.email.postfix" = "Dowiedz się więcej"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_pt-BR.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Carregando..."; 9 | "button.footer.works_with" = "FUNCIONA COM"; 10 | "button.state.verifying" = "Verificando..."; 11 | "button.state.creating_account" = "Criando conta..."; 12 | "button.footer.accountCreation" = "Nova conta do IFTTT para %@"; 13 | "button.state.sign_in" = "Indo para %@..."; 14 | "button.state.sign_in.brief" = "Entrar"; 15 | "button.state.saving_configuration" = "Salvando configurações..."; 16 | "button.state.connected" = "Conectado"; 17 | "button.state.disconnect" = "Deslize para desconectar"; 18 | "button.state.disconnecting" = "Desconectando..."; 19 | "button.state.disconnected" = "Desconectado"; 20 | "button.email.placeholder" = "Seu e-mail"; 21 | "button.footer.email.invalid" = "Insira um e-mail válido"; 22 | "button.footer.loading.failed.postfix" = "Tentar novamente"; 23 | "button.state.connect" = "Conectar %@"; 24 | "button.state.connecting" = "Conectando"; 25 | "button.footer.email.prefix" = "Seguro com o IFTTT. Saiba mais"; 26 | "about.title_full" = "O IFTTT conecta %@ a %@"; 27 | "about.connect" = "Libere novos recursos quando seus produtos favoritos funcionam em conjunto"; 28 | "about.control" = "Controle quais serviços acessam seus dados e como eles os usam"; 29 | "about.security" = "Proteja e monitore seus dados"; 30 | "about.manage" = "Desconecte a qualquer momento"; 31 | "about.legal.full" = "Ao usar esta conexão, você concorda com nossa Privacidade e nossos Termos"; 32 | "button.footer.loading.failed.prefix" = "Verifique se o telefone tem conexão com a Internet"; 33 | "about.connection-deep-link" = "Gerenciar"; 34 | "about.legal.link" = "Privacidade e nossos Termos"; 35 | "button.footer.email.postfix" = "Saiba mais"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_pt-PT.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "A carregar"; 9 | "button.footer.works_with" = "FUNCIONA COM"; 10 | "button.state.verifying" = "A verificar..."; 11 | "button.state.creating_account" = "A criar conta..."; 12 | "button.footer.accountCreation" = "Nova conta IFTTT para %@"; 13 | "button.state.sign_in" = "A ir para %@..."; 14 | "button.state.sign_in.brief" = "Iniciar sessão"; 15 | "button.state.saving_configuration" = "A guardar definições..."; 16 | "button.state.connected" = " ligado"; 17 | "button.state.disconnect" = "Deslize para desligar"; 18 | "button.state.disconnecting" = "A desligar..."; 19 | "button.state.disconnected" = "Desligado"; 20 | "button.email.placeholder" = "O seu e-mail"; 21 | "button.footer.email.invalid" = "Introduza um e-mail válido"; 22 | "button.footer.loading.failed.postfix" = "Repetir"; 23 | "button.state.connect" = "Estabelecer ligação a %@"; 24 | "button.state.connecting" = "A ligar"; 25 | "button.footer.email.prefix" = "Protegido com IFTTT. Mais informações"; 26 | "about.title_full" = "IFTTT liga %@ para %@"; 27 | "about.connect" = "Desbloqueie novas funcionalidades quando os seus equipamentos favoritos trabalham em conjunto"; 28 | "about.control" = "Controle que serviços acedem aos seus dados e como utilizá-los"; 29 | "about.security" = "Proteja e monitorize os seus dados"; 30 | "about.manage" = "Desligue em qualquer altura"; 31 | "about.legal.full" = "A utilizar esta ligação, concorda com os nossos Termos e Privacidade"; 32 | "button.footer.loading.failed.prefix" = "Certifique-se de que o seu telefone tem ligação à Internet"; 33 | "about.connection-deep-link" = "Gerir"; 34 | "about.legal.link" = "Termos e Privacidade"; 35 | "button.footer.email.postfix" = "Mais informações"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_ru.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Загрузка..."; 9 | "button.footer.works_with" = "РАБОТАЕТ С"; 10 | "button.state.verifying" = "Проверка..."; 11 | "button.state.creating_account" = "Создание учетной записи..."; 12 | "button.footer.accountCreation" = "Новая учетная запись IFTTT для %@"; 13 | "button.state.sign_in" = "Переход к %@..."; 14 | "button.state.sign_in.brief" = "Войти"; 15 | "button.state.saving_configuration" = "Сохранение настроек..."; 16 | "button.state.connected" = "Подключено: "; 17 | "button.state.disconnect" = "Проведите для отключения"; 18 | "button.state.disconnecting" = "Отключение..."; 19 | "button.state.disconnected" = "Отключено"; 20 | "button.email.placeholder" = "Ваш адрес электронной почты"; 21 | "button.footer.email.invalid" = "Введите действительный адрес"; 22 | "button.footer.loading.failed.postfix" = "Повторить попытку"; 23 | "button.state.connect" = "Подключить %@"; 24 | "button.state.connecting" = "Подключение"; 25 | "button.footer.email.prefix" = "Под защитой IFTTT. Узнайте больше"; 26 | "about.title_full" = "IFTTT подключает %@ к %@"; 27 | "about.connect" = "Откройте новые возможности благодаря взаимодействию ваших любимых вещей"; 28 | "about.control" = "Контролируйте доступ различных служб к вашим данным и их использование"; 29 | "about.security" = "Защищайте и отслеживайте свои данные"; 30 | "about.manage" = "Вы можете отключиться в любой момент!"; 31 | "about.legal.full" = "Используя это подключение, вы принимаете наши Политику конфиденциальности и Условия"; 32 | "button.footer.loading.failed.prefix" = "Убедитесь, что телефон подключен к Интернету"; 33 | "about.connection-deep-link" = "Управление"; 34 | "about.legal.link" = "конфиденциальности и Условия"; 35 | "button.footer.email.postfix" = "Узнайте больше"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_sv.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "Läser in ..."; 9 | "button.footer.works_with" = "FUNGERAR MED"; 10 | "button.state.verifying" = "Verifierar…"; 11 | "button.state.creating_account" = "Skapar konto…"; 12 | "button.footer.accountCreation" = "Nytt IFTTT-konto för %@"; 13 | "button.state.sign_in" = "Går till %@…"; 14 | "button.state.sign_in.brief" = "Logga in"; 15 | "button.state.saving_configuration" = "Spara inställningar…"; 16 | "button.state.connected" = "Ansluten till "; 17 | "button.state.disconnect" = "Skjut för att koppla från"; 18 | "button.state.disconnecting" = "Kopplar från…"; 19 | "button.state.disconnected" = "Frånkopplad"; 20 | "button.email.placeholder" = "Din e-postadress"; 21 | "button.footer.email.invalid" = "Ange en giltig e-postadress"; 22 | "button.footer.loading.failed.postfix" = "Försök igen"; 23 | "button.state.connect" = "Anslut %@"; 24 | "button.state.connecting" = "Ansluter"; 25 | "button.footer.email.prefix" = "Skyddad med IFTTT. Läs mer"; 26 | "about.title_full" = "IFTTT ansluter %@ till %@"; 27 | "about.connect" = "Upptäck nya funktioner när dina favoritsaker arbetar tillsammans"; 28 | "about.control" = "Styr vilka tjänster som kommer åt dina uppgifter och hur de använder dem"; 29 | "about.security" = "Skydda och övervaka dina uppgifter"; 30 | "about.manage" = "Koppla ur när som helst"; 31 | "about.legal.full" = "Genom att använda den här anslutningen accepterar du vår Sekretesspolicy och våra villkor"; 32 | "button.footer.loading.failed.prefix" = "Kontrollera att din telefon har en Internetanslutning"; 33 | "about.connection-deep-link" = "Hantera"; 34 | "about.legal.link" = "Sekretesspolicy och våra villkor"; 35 | "button.footer.email.postfix" = "Läs mer"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_zh-Hans.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "正在加载..."; 9 | "button.footer.works_with" = "使用"; 10 | "button.state.verifying" = "正在验证..."; 11 | "button.state.creating_account" = "正在创建帐户..."; 12 | "button.footer.accountCreation" = "为 %@ 新建 IFTTT 帐户"; 13 | "button.state.sign_in" = "正在转到 %@..."; 14 | "button.state.sign_in.brief" = "登录"; 15 | "button.state.saving_configuration" = "正在保存设置..."; 16 | "button.state.connected" = "已连接"; 17 | "button.state.disconnect" = "滑动即可断开"; 18 | "button.state.disconnecting" = "正在断开连接..."; 19 | "button.state.disconnected" = "已断开连接"; 20 | "button.email.placeholder" = "您的邮箱"; 21 | "button.footer.email.invalid" = "输入有效的邮箱"; 22 | "button.footer.loading.failed.postfix" = "重试"; 23 | "button.state.connect" = "连接 %@"; 24 | "button.state.connecting" = "正在连接"; 25 | "button.footer.email.prefix" = "IFTTT 提供保护。了解更多"; 26 | "about.title_full" = "IFTTT 将 %@ 连接至 %@"; 27 | "about.connect" = "常用工具齐登场,助力解锁新功能。"; 28 | "about.control" = "控制可访问您数据的服务,并管理其使用方式"; 29 | "about.security" = "保护和监控您的数据"; 30 | "about.manage" = "可随时断电"; 31 | "about.legal.full" = "使用此连接,即表示您同意我们的隐私条款"; 32 | "button.footer.loading.failed.prefix" = "确保您的手机已连接互联网"; 33 | "about.connection-deep-link" = "管理"; 34 | "about.legal.link" = "隐私条款"; 35 | "button.footer.email.postfix" = "了解更多"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Resources/Localizable_zh-Hant.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | IFTTT SDK 4 | 5 | Copyright © 2018 IFTTT. All rights reserved. 6 | */ 7 | 8 | "button.state.loading" = "正在載入..."; 9 | "button.footer.works_with" = "搭配使用"; 10 | "button.state.verifying" = "驗證中..."; 11 | "button.state.creating_account" = "正在建立帳戶..."; 12 | "button.footer.accountCreation" = "%@​ 的 IFTTT 新帳戶"; 13 | "button.state.sign_in" = "正在前往 %@..."; 14 | "button.state.sign_in.brief" = "登入"; 15 | "button.state.saving_configuration" = "正在儲存設定..."; 16 | "button.state.connected" = "已連線"; 17 | "button.state.disconnect" = "滑動以中斷連線"; 18 | "button.state.disconnecting" = "正在中斷連線..."; 19 | "button.state.disconnected" = "已中斷連線"; 20 | "button.email.placeholder" = "您的電子郵件"; 21 | "button.footer.email.invalid" = "輸入有效的電子郵件"; 22 | "button.footer.loading.failed.postfix" = "重試"; 23 | "button.state.connect" = "連線 %@"; 24 | "button.state.connecting" = "正在連線"; 25 | "button.footer.email.prefix" = "透過 IFTTT 保護。了解更多"; 26 | "about.title_full" = "IFTTT 會將 %@ 連線至 %@"; 27 | "about.connect" = "讓您喜愛的事物彼此合作,就能發掘出新功能"; 28 | "about.control" = "控制哪些服務可存取您的資料,以及使用資料的方式"; 29 | "about.security" = "保護並監控您的資料"; 30 | "about.manage" = "隨時斷開"; 31 | "about.legal.full" = "使用此連線,即表示您同意我們的隱私權與條款"; 32 | "button.footer.loading.failed.prefix" = "請確定您的手機有網際網路連線"; 33 | "about.connection-deep-link" = "管理"; 34 | "about.legal.link" = "隱私權與條款"; 35 | "button.footer.email.postfix" = "了解更多"; 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Result+Queries.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | #if swift(<5.0) 9 | /// An object to model success and failure states from an API. 10 | public enum Result { 11 | /// The operation was successful. The passed associated value is the result that was returned from the API. 12 | case success(ValueType) 13 | 14 | /// The operation failed. The passed associated value is the error that was encountered. 15 | case failure(ErrorType) 16 | 17 | /// An alias for `ValueType`. 18 | typealias Success = ValueType 19 | 20 | /// An alias for `ErrorType`. 21 | typealias Failure = ErrorType 22 | } 23 | #endif 24 | 25 | extension Result { 26 | /// The associated value for `success`es. Returns `nil` on `failure`. 27 | var value: Success? { 28 | switch self { 29 | case let .success(value): 30 | return value 31 | case .failure: 32 | return nil 33 | } 34 | } 35 | 36 | /// The associated error for `failure`s. Returns `nil` on `success`. 37 | var error: Failure? { 38 | switch self { 39 | case .success: 40 | return nil 41 | case.failure(let error): 42 | return error 43 | } 44 | } 45 | 46 | /// Whether the receiver is the `.success` case. 47 | var isSuccess: Bool { 48 | switch self { 49 | case .success: 50 | return true 51 | case .failure: 52 | return false 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /IFTTT SDK/SFWebServiceAuthentication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SFWebServiceAuthentication.swift 3 | // IFTTTConnectSDK 4 | // 5 | // Copyright © 2021 IFTTT. All rights reserved. 6 | // 7 | 8 | import SafariServices 9 | 10 | /// Wraps an `SFAuthenticationSession`. Used to authenticate web services up to (not including) iOS 12. 11 | @available(iOS 11.0, *) 12 | @available(iOS, deprecated: 12, obsoleted: 13, message: "API is deprecated in iOS 12 and obsoleted in iOS 13") 13 | final class SFWebService: WebServiceAuthentication { 14 | 15 | /// The backing `SFAuthenticationSession` object. 16 | private var session: SFAuthenticationSession? 17 | 18 | @discardableResult 19 | override func start(with parameters: Parameters, completionHandler: @escaping (Result) -> Void) -> Bool { 20 | let sfAuthenticationSession = SFAuthenticationSession(url: parameters.url, 21 | callbackURLScheme: parameters.callbackURLScheme) { url, error in 22 | guard error == nil, let url = url else { 23 | completionHandler(.failure(.userCanceled)) 24 | return 25 | } 26 | completionHandler(.success(url)) 27 | } 28 | self.session = sfAuthenticationSession 29 | return sfAuthenticationSession.start() 30 | } 31 | 32 | override func cancel() { 33 | session?.cancel() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /IFTTT SDK/Selectable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Selectable.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class Selectable: NSObject, UIGestureRecognizerDelegate { 11 | var isEnabled: Bool { 12 | get { return gesture.isEnabled } 13 | set { gesture.isEnabled = newValue } 14 | } 15 | 16 | /// Customize the behavior when the Selectable view is highlighted 17 | var performHighlight: SelectGestureRecognizer.HighlightHandler? { 18 | get { 19 | return gesture.performHighlight 20 | } 21 | set { 22 | gesture.performHighlight = newValue 23 | } 24 | } 25 | 26 | private let gesture = SelectGestureRecognizer() 27 | private var onSelect: VoidClosure 28 | 29 | init(_ view: UIView, onSelect: @escaping VoidClosure) { 30 | self.onSelect = onSelect 31 | 32 | super.init() 33 | 34 | gesture.addTarget(self, action: #selector(handleSelect(_:))) 35 | gesture.delaysTouchesBegan = true 36 | gesture.delegate = self 37 | 38 | view.addGestureRecognizer(gesture) 39 | view.isUserInteractionEnabled = true 40 | } 41 | 42 | @objc private func handleSelect(_ gesture: UIGestureRecognizer) { 43 | if gesture.state == .ended { 44 | onSelect() 45 | } 46 | } 47 | 48 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 49 | return true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /IFTTT SDK/ServiceIconsNetworkController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceIconsNetworkController.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Implementation of `ImageViewNetworkController` to download service icons 11 | class ServiceIconsNetworkController: ImageViewNetworkController { 12 | private let downloader = ImageDownloader() 13 | 14 | /// Prefetch and cache service icon images for this `Connection`. 15 | /// This will be it very unlikely that an image is delayed and visually "pops in". 16 | func prefetchImages(for connection: Connection) { 17 | connection.services.forEach { 18 | downloader.downloadImage(url: $0.templateIconURL, { _ in}) 19 | } 20 | } 21 | 22 | private var currentRequests = [UIImageView : URLSessionDataTask]() 23 | 24 | func setImage(with url: URL?, for imageView: UIImageView) { 25 | if let existingTask = currentRequests[imageView] { 26 | if existingTask.originalRequest?.url == url { 27 | return // This is a duplicate request 28 | } else { 29 | // The image url was changed 30 | existingTask.cancel() 31 | currentRequests[imageView] = nil 32 | } 33 | } 34 | 35 | imageView.image = nil 36 | if let url = url { 37 | let task = downloader.downloadImage(url: url) { (result) in 38 | switch result { 39 | case .success(let image): 40 | imageView.image = image 41 | case .failure: 42 | // Images that fail to load are left blank 43 | // Since we attempt to precatch images, this is actually the second try 44 | break 45 | } 46 | } 47 | currentRequests[imageView] = task 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /IFTTT SDK/Set+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Set+Helpers.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Set { 11 | func map(transform: (Element) -> U) -> Set { 12 | return Set(lazy.map(transform)) 13 | } 14 | 15 | func compactMap(_ transform: (Element) -> U?) -> Set { 16 | return Set(lazy.compactMap(transform)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /IFTTT SDK/SynchronizationTriggerEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SynchronizationSubscriber.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Describes a closure that's invoked after a sync subscriber finishes its task 11 | /// 12 | /// - Parameters: 13 | /// - backgroundFetchResult: An instance of `UIBackgroundFetchResult` which describes ahat the result of the fetch result is. 14 | /// - authenticationFailure: A bool which determines whether or not the sync operation resulted in an authentication failure. 15 | typealias BackgroundFetchClosure = (UIBackgroundFetchResult, Bool) -> Void 16 | 17 | /// Defines an event which should trigger a background synchronization 18 | struct SynchronizationTriggerEvent { 19 | 20 | /// The source of the synchronization trigger event 21 | let source: SynchronizationSource 22 | 23 | /// The associated background fetch completion handler. This relates to background handler for push notifications. 24 | let completionHandler: BackgroundFetchClosure? 25 | } 26 | -------------------------------------------------------------------------------- /IFTTT SDK/UIKit+ConvenienceInit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKit+ConvenienceInit.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UILabel { 11 | convenience init(_ text: String? = nil, _ configure: ((UILabel) -> Void)? = nil) { 12 | self.init() 13 | self.text = text 14 | self.adjustsFontForContentSizeCategory = true 15 | configure?(self) 16 | } 17 | convenience init(_ attributedText: NSAttributedString?, _ configure: ((UILabel) -> Void)? = nil) { 18 | self.init() 19 | self.attributedText = attributedText 20 | self.adjustsFontForContentSizeCategory = true 21 | configure?(self) 22 | } 23 | } 24 | 25 | extension UIStackView { 26 | convenience init(_ views: [UIView] = [], _ configure: ((UIStackView) -> Void)) { 27 | self.init(arrangedSubviews: views) 28 | configure(self) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /IFTTT SDK/URLComponents+email.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLComponents+email.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLComponents { 11 | func fixingEmailEncoding() -> URLComponents { 12 | var components = self 13 | components.fixEmailEncoding() 14 | return components 15 | } 16 | 17 | mutating func fixEmailEncoding() { 18 | // We need to manually encode `+` characters in a user's e-mail because `+` is a valid character that represents a space in a url query. E-mail's with spaces are not valid. 19 | let updatedQuery = percentEncodedQuery?.addingPercentEncoding(withAllowedCharacters: .emailEncodingPassthrough) 20 | self.percentEncodedQuery = updatedQuery 21 | } 22 | } 23 | 24 | private extension CharacterSet { 25 | 26 | /// This allows '+' character to passthrough for sending an email address as a url parameter. 27 | static let emailEncodingPassthrough = CharacterSet(charactersIn: "+").inverted 28 | } 29 | -------------------------------------------------------------------------------- /IFTTT SDK/URLRequest+CommonValues.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest+CommonValues.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLRequest { 11 | 12 | struct HeaderFields { 13 | static let inviteCode = "IFTTT-Invite-Code" 14 | static let sdkVersion = "IFTTT-SDK-Version" 15 | static let sdkPlatform = "IFTTT-SDK-Platform" 16 | static let sdkAnonymousId = "IFTTT-SDK-Anonymous-Id" 17 | } 18 | 19 | mutating func addIftttServiceToken(_ token: String) { 20 | let tokenString = "Bearer \(token)" 21 | addValue(tokenString, forHTTPHeaderField: "Authorization") 22 | } 23 | 24 | mutating func addIftttInviteCode(_ code: String) { 25 | addValue(code, forHTTPHeaderField: HeaderFields.inviteCode) 26 | } 27 | 28 | mutating func addVersionTracking() { 29 | setValue(API.sdkVersion, forHTTPHeaderField: HeaderFields.sdkVersion) 30 | setValue(API.sdkPlatform, forHTTPHeaderField: HeaderFields.sdkPlatform) 31 | setValue(API.anonymousId, forHTTPHeaderField: HeaderFields.sdkAnonymousId) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /IFTTT SDK/URLSession+JSONTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSession+JSONTask.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLSession { 11 | static let connectionURLSession: URLSession = { 12 | let configuration = URLSessionConfiguration.ephemeral 13 | configuration.httpAdditionalHeaders = ["Accept" : "application/json"] 14 | return URLSession(configuration: configuration) 15 | }() 16 | 17 | func jsonTask(with urlRequest: URLRequest, _ completion: @escaping (Parser, HTTPURLResponse?, Error?) -> Void) -> URLSessionDataTask { 18 | return dataTask(with: urlRequest) { (data, response, error) in 19 | DispatchQueue.main.async { 20 | completion(Parser(content: data), response as? HTTPURLResponse, error) 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /IFTTT SDK/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | struct User { 11 | enum Id { 12 | case username(String), email(String) 13 | 14 | var value: String { 15 | switch self { 16 | case .username(let username): 17 | return username 18 | case .email(let email): 19 | return email 20 | } 21 | } 22 | } 23 | 24 | /// We know something about a user when they begin a connect button flow. 25 | /// This type tells the `ConnectionNetworkController` how to identify the user's IFTTT account. 26 | /// If we have a token, there must be an associate account. If we only have an email, they could be new to IFTTT. 27 | /// The `ConnectionNetworkController` resolves this to a `User` instance. 28 | /// 29 | /// - token: The user is already logged in to IFTTT and we have an IFTTT service user token 30 | /// - email: The user is not logged in but we know their email address 31 | enum LookupMethod { 32 | case token(String), email(String) 33 | } 34 | 35 | let id: User.Id 36 | let isExistingUser: Bool 37 | } 38 | 39 | extension String { 40 | 41 | /// Checks whether a string contains a e-mail address. 42 | /// 43 | /// - Returns: A `Bool` on whether a e-mail address is present. 44 | /// - Throws: If a `NSDataDetector` can not be created. 45 | var isValidEmail: Bool { 46 | let types: NSTextCheckingResult.CheckingType = [.link] 47 | guard let detector = try? NSDataDetector(types: types.rawValue) else { 48 | 49 | // Returning true because the data detectored failed to initalized and we can not validate the e-mail. Erroring on the side of the e-mail being valid in this case. 50 | return true 51 | } 52 | 53 | let range = NSRange(location: 0, length: count) 54 | let matches = detector.matches(in: self, options: [], range: range) 55 | 56 | guard matches.count == 1, let result = matches.first, range == result.range else { return false } 57 | return result.url?.scheme == "mailto" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /IFTTT SDK/WebServiceAuthentication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebServiceAuthentication.swift 3 | // IFTTT SDK 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import AuthenticationServices 9 | 10 | /// Describes the error reason for a failed `WebServiceAuthentication` 11 | /// 12 | /// - userCanceled: The user cancelled the authentication session. 13 | /// - failed: The authentication failed for some reason. 14 | /// - invalidResponse: The response returned by the web service was invalid. 15 | /// - notHandled: The error wasn't handled by the web service. 16 | /// - presentationContextInvalid: The presentation content provided was invalid. 17 | /// - unknown: Some unknown error ocurred when authenticating with the web service. 18 | enum AuthenticationError: Error { 19 | case userCanceled 20 | case failed 21 | case invalidResponse 22 | case notHandled 23 | case presentationContextInvalid 24 | case unknown 25 | } 26 | 27 | /// Describes a generic method of authenticating with a service 28 | protocol ServiceAuthentication { 29 | /// The parameters to pass to authenticate with the service 30 | associatedtype Parameters 31 | /// The object type on a successful service authentication 32 | associatedtype Completion 33 | /// THe error type on a non-successful service authentication 34 | associatedtype ErrorType: Error 35 | 36 | /// Describes a closure that gets invoked on success/failure of a service authentication 37 | typealias AuthenticationSessionClosure = ((Result) -> Void) 38 | 39 | /// Starts the service authentication. 40 | /// - Parameters: 41 | /// - parameters: An instance of `Parameters` which can be used in authenticating against the service. 42 | /// - completionHandler: The closure to invoke on a success or failure response from the service. 43 | /// - Returns: A `Bool` value as to whether or not the service authentication was started or not. 44 | func start(with parameters: Parameters, completionHandler: @escaping AuthenticationSessionClosure) -> Bool 45 | 46 | /// Cancels the service authentication. 47 | func cancel() 48 | } 49 | 50 | /// A basic OAuth web service authentication object. 51 | class WebServiceAuthentication: ServiceAuthentication { 52 | 53 | /// The parameters used when authenticating against a web service 54 | struct WebServiceAuthenticationParameters { 55 | /// The URL to use in authenticating the service 56 | let url: URL 57 | /// The callback url scheme which the service uses to pass back any data 58 | let callbackURLScheme: String? 59 | /// Determines whether or not the session should be ephemeral or not. Not all service authentication types support this 60 | let prefersEphemeralWebBrowserSession: Bool 61 | } 62 | 63 | typealias Parameters = WebServiceAuthenticationParameters 64 | typealias Completion = URL 65 | typealias ErrorType = AuthenticationError 66 | 67 | @discardableResult 68 | func start(with parameters: Parameters, completionHandler: @escaping AuthenticationSessionClosure) -> Bool { 69 | fatalError("This class must be subclassed.") 70 | } 71 | 72 | func cancel() { 73 | fatalError("This class must be subclassed.") 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /IFTTTConnectSDK.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "IFTTTConnectSDK" 3 | spec.version = "2.8.0" 4 | spec.summary = "Allows your users to activate programmable IFTTT Connections directly in your app." 5 | spec.description = <<-DESC 6 | - Easily authenticate your services to IFTTT through the Connect Button 7 | - Configure the Connect Button through code or through interface builder with IBDesignable 8 | - Configure the ConnectButtonController to handle the Connection activation flow 9 | DESC 10 | spec.homepage = "https://github.com/IFTTT/ConnectSDK-iOS" 11 | spec.license = { :type => "MIT", :file => "LICENSE" } 12 | spec.author = { "Siddharth Sathyam" => "siddharth@ifttt.com" } 13 | spec.platform = :ios, "10.0" 14 | spec.swift_version = "5.0" 15 | spec.source = { :git => "https://github.com/IFTTT/ConnectSDK-iOS.git", :tag => "#{spec.version}" } 16 | spec.source_files = "IFTTT SDK/**/*.swift" 17 | spec.resource_bundles = { 18 | 'IFTTTConnectSDK' => ['IFTTT SDK/Resources/Assets.xcassets'], 19 | 'IFTTTConnectSDK-Localizations' => ['IFTTT SDK/Resources/*.strings'] 20 | } 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 IFTTT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ConnectSDK-iOS", 6 | platforms: [ 7 | .iOS(.v10) 8 | ], 9 | products: [ 10 | .library(name: "IFTTTConnectSDK", targets: ["IFTTTConnectSDK"]) 11 | ], 12 | targets: [ 13 | .target( 14 | name: "IFTTTConnectSDK", 15 | path: "IFTTT SDK", 16 | exclude: ["Info.plist"], 17 | resources: [.process("Resources")] 18 | ), 19 | .testTarget( 20 | name: "SDKHostAppTests", 21 | dependencies: ["IFTTTConnectSDK"], 22 | path: "SDKHostAppTests", 23 | exclude: ["Info.plist"], 24 | resources: [.process("fetch_connection_response.json")] 25 | ) 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /SDKHostApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SDKHostApp 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 13 | // Override point for customization after application launch. 14 | return true 15 | } 16 | 17 | // MARK: UISceneSession Lifecycle 18 | 19 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 20 | // Called when a new scene session is being created. 21 | // Use this method to select a configuration to create the new scene with. 22 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 23 | } 24 | 25 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 26 | // Called when the user discards a scene session. 27 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 28 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 29 | } 30 | 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /SDKHostApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SDKHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SDKHostApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SDKHostApp/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /SDKHostApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SDKHostApp 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | Text("Hello, world!") 13 | .padding() 14 | } 15 | } 16 | 17 | struct ContentView_Previews: PreviewProvider { 18 | static var previews: some View { 19 | ContentView() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SDKHostApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UIApplicationSupportsIndirectInputEvents 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /SDKHostApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SDKHostApp/SDKHostApp.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | keychain-access-groups 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SDKHostApp/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // SDKHostApp 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | 21 | // Create the SwiftUI view that provides the window contents. 22 | let contentView = ContentView() 23 | 24 | // Use a UIHostingController as window root view controller. 25 | if let windowScene = scene as? UIWindowScene { 26 | let window = UIWindow(windowScene: windowScene) 27 | window.rootViewController = UIHostingController(rootView: contentView) 28 | self.window = window 29 | window.makeKeyAndVisible() 30 | } 31 | } 32 | 33 | func sceneDidDisconnect(_ scene: UIScene) { 34 | // Called as the scene is being released by the system. 35 | // This occurs shortly after the scene enters the background, or when its session is discarded. 36 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 37 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 38 | } 39 | 40 | func sceneDidBecomeActive(_ scene: UIScene) { 41 | // Called when the scene has moved from an inactive state to an active state. 42 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 43 | } 44 | 45 | func sceneWillResignActive(_ scene: UIScene) { 46 | // Called when the scene will move from an active state to an inactive state. 47 | // This may occur due to temporary interruptions (ex. an incoming phone call). 48 | } 49 | 50 | func sceneWillEnterForeground(_ scene: UIScene) { 51 | // Called as the scene transitions from the background to the foreground. 52 | // Use this method to undo the changes made on entering the background. 53 | } 54 | 55 | func sceneDidEnterBackground(_ scene: UIScene) { 56 | // Called as the scene transitions from the foreground to the background. 57 | // Use this method to save data, release shared resources, and store enough scene-specific state information 58 | // to restore the scene back to its current state. 59 | } 60 | 61 | 62 | } 63 | 64 | -------------------------------------------------------------------------------- /SDKHostAppTests/ArrayHelpersTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayHelpersTests.swift 3 | // SDKHostAppTests 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | import CoreLocation 10 | 11 | @testable import IFTTTConnectSDK 12 | 13 | class ArrayHelpersTests: XCTestCase { 14 | func testClosestRegionsToLocation() throws { 15 | let regions = LocationTestHelpers.generateRegions(withStartCoordinate: LocationTestHelpers.IFTTTCenterCoordinate, 16 | count: 50, 17 | radius: 100) 18 | let iftttLocation = LocationTestHelpers.IFTTTCenterCoordinate 19 | 20 | let twentyClosestRegions = regions 21 | .shuffled() 22 | .closestRegions(to: iftttLocation, 23 | count: 20) 24 | 25 | XCTAssert(regions.keepFirst(20) == twentyClosestRegions) 26 | 27 | let thirtyClosestRegions = regions 28 | .shuffled() 29 | .closestRegions(to: iftttLocation, 30 | count: 30) 31 | XCTAssert(regions.keepFirst(30) == thirtyClosestRegions) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SDKHostAppTests/CLRegion+Parsing_spec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLRegion+Parsing_spec.swift 3 | // IFTTT SDKTests 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | import CoreLocation 10 | 11 | @testable import IFTTTConnectSDK 12 | 13 | class CLRegion_Parsing_spec: XCTestCase { 14 | 15 | func testIFTTTRegion() { 16 | let iftttRegion = CLCircularRegion(center: .init(latitude: 0.0, longitude: 0.0), radius: 100, identifier: "ifttt_somecoolidentifier") 17 | let nonIftttRegion = CLCircularRegion(center: .init(latitude: 10.0, longitude: 10.0), radius: 100, identifier: "somecoolnoniftttidentifier") 18 | 19 | assert(iftttRegion.isIFTTTRegion) 20 | assert(!nonIftttRegion.isIFTTTRegion) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SDKHostAppTests/EventPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventPublisherTests.swift 3 | // IFTTT SDKTests 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import IFTTTConnectSDK 11 | 12 | class EventPublisherTests: XCTestCase { 13 | var publisher: EventPublisher! 14 | 15 | override func setUp() { 16 | publisher = EventPublisher() 17 | } 18 | 19 | func testAddSubscriber() { 20 | let addSubscriberExpectation = expectation(description: "verify_added_subscriber_gets_invoked_with_value") 21 | addSubscriberExpectation.assertForOverFulfill = true 22 | publisher.addSubscriber { value in 23 | addSubscriberExpectation.fulfill() 24 | } 25 | publisher.onNext(1) 26 | wait(for: [addSubscriberExpectation], timeout: 1.0) 27 | } 28 | 29 | func testPublish() { 30 | let expectationCount = 100 31 | let expectations = (0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SDKHostAppTests/String_EmailDataDetectorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+EmailDataDetectorTests.swift 3 | // IFTTT SDKTests 4 | // 5 | // Copyright © 2019 IFTTT. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | @testable import IFTTTConnectSDK 10 | 11 | class StringEmailDataDetectorTests: XCTestCase { 12 | let emailString = "hello@ifttt.com" 13 | let nonEmailString = "Ten minutes left to execute our plan. Where’s everyone else? Playing Spiderman." 14 | let blockMixedText = "Ten minutes left to execute our plan. Where’s everyone else? Playing Spiderman. Reach out to hello@ifttt.com" 15 | let anotherBlockMixedText = "hello@ifttt.com Ten minutes left to execute our plan. Where’s everyone else? Playing Spiderman." 16 | let multipleEmails = "hello@ifttt.com, feedback@ifttt.com" 17 | let nonTraditionalEmailAddress = "hello+mike.amu_dsen@ifttt.com" 18 | 19 | func testIsEmail() { 20 | XCTAssertEqual(emailString.isValidEmail, true) 21 | XCTAssertEqual(nonEmailString.isValidEmail, false) 22 | XCTAssertEqual(blockMixedText.isValidEmail, false) 23 | XCTAssertEqual(anotherBlockMixedText.isValidEmail, false) 24 | XCTAssertEqual(multipleEmails.isValidEmail, false) 25 | XCTAssertEqual(nonTraditionalEmailAddress.isValidEmail, true) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SDKHostAppTests/SynchronizationSchedulerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SynchronizationSchedulerTests.swift 3 | // SDKHostAppTests 4 | // 5 | // Copyright © 2021 IFTTT. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | @testable import IFTTTConnectSDK 12 | 13 | class SynchronizationSchedulerTests: XCTestCase { 14 | private var syncScheduler: SynchronizationScheduler! 15 | private var eventPublisher: EventPublisher! 16 | 17 | override func setUp() { 18 | eventPublisher = .init() 19 | syncScheduler = .init(manager: .init(subscribers: []), triggers: eventPublisher) 20 | } 21 | 22 | override func tearDown() { 23 | eventPublisher = nil 24 | syncScheduler = nil 25 | } 26 | 27 | func test_start() { 28 | syncScheduler.stop() 29 | syncScheduler.start(lifecycleSynchronizationOptions: .all) 30 | 31 | XCTAssertNotNil(syncScheduler.subscriberToken) 32 | XCTAssertTrue(!syncScheduler.applicationLifecycleNotificationCenterTokens.isEmpty) 33 | XCTAssertTrue(!syncScheduler.sdkGeneratedNotificationCenterTokens.isEmpty) 34 | } 35 | 36 | func test_stop() { 37 | syncScheduler.start(lifecycleSynchronizationOptions: .all) 38 | syncScheduler.stop() 39 | 40 | XCTAssertNil(syncScheduler.subscriberToken) 41 | XCTAssertTrue(syncScheduler.applicationLifecycleNotificationCenterTokens.isEmpty) 42 | XCTAssertTrue(syncScheduler.sdkGeneratedNotificationCenterTokens.isEmpty) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SDKHostAppUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SDKHostAppUITests/SDKHostAppUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDKHostAppUITests.swift 3 | // SDKHostAppUITests 4 | // 5 | // Copyright © 2020 IFTTT. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | 10 | class SDKHostAppUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | --------------------------------------------------------------------------------