├── .github ├── pull_request_template.md └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .swiftlint.yml ├── APNS ├── APNS.podspec ├── APNS.xcodeproj │ └── project.pbxproj ├── APNS │ ├── APNS.h │ ├── APNSIdentity.swift │ ├── APNSPusher.swift │ ├── APNSSecIdentityType.swift │ ├── APNSServiceBrowser.swift │ ├── APNSServiceDevice.swift │ ├── Info.plist │ ├── ShellRunner.swift │ └── URLRequest+Extensions.swift └── APNSTests │ ├── APNSServiceDeviceSpec.swift │ ├── Info.plist │ └── URLRequestSpec.swift ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEPLOY.md ├── FCM ├── FCM.podspec └── FCM │ ├── FCMPusher.swift │ ├── Info.plist │ ├── String+Slice.swift │ └── URLRequest+FCM.swift ├── Gemfile ├── LICENSE ├── Podfile ├── PusherMainView ├── Highlight.framework │ ├── Headers │ ├── Highlight │ ├── Modules │ ├── Resources │ └── Versions │ │ ├── A │ │ ├── Headers │ │ │ └── Highlight-Swift.h │ │ ├── Highlight │ │ ├── Modules │ │ │ ├── Highlight.swiftmodule │ │ │ │ ├── arm64-apple-macos.abi.json │ │ │ │ ├── arm64-apple-macos.private.swiftinterface │ │ │ │ ├── arm64-apple-macos.swiftdoc │ │ │ │ ├── arm64-apple-macos.swiftinterface │ │ │ │ ├── arm64-apple-macos.swiftmodule │ │ │ │ ├── x86_64-apple-macos.abi.json │ │ │ │ ├── x86_64-apple-macos.private.swiftinterface │ │ │ │ ├── x86_64-apple-macos.swiftdoc │ │ │ │ ├── x86_64-apple-macos.swiftinterface │ │ │ │ └── x86_64-apple-macos.swiftmodule │ │ │ └── module.modulemap │ │ ├── Resources │ │ │ └── Info.plist │ │ └── _CodeSignature │ │ │ └── CodeResources │ │ └── Current ├── PusherMainView.podspec ├── PusherMainView.xcodeproj │ └── project.pbxproj ├── PusherMainView │ ├── ActionType.swift │ ├── AuthTokenViewcontroller.swift │ ├── Base.lproj │ │ └── Pusher.storyboard │ ├── DefaultPayloads.swift │ ├── DevicesViewController.swift │ ├── Dictionary+Utils.swift │ ├── ErrorState.swift │ ├── Info.plist │ ├── InterfaceFactory.swift │ ├── JSONTextView.swift │ ├── Keychain.swift │ ├── Localizable.strings │ ├── MainPlayground.playground │ │ ├── Contents.swift │ │ └── contents.xcplayground │ ├── NSApplication+Extensions.swift │ ├── PlistFinder.swift │ ├── PushData.swift │ ├── PushTesterError.swift │ ├── PushTypesViewController.swift │ ├── PusherMainView.h │ ├── PusherReducer.swift │ ├── PusherState.swift │ ├── PusherStore.swift │ ├── PusherViewController.swift │ ├── Router.swift │ ├── String+JSON.swift │ ├── String+Localization.swift │ ├── String+Subscript.swift │ ├── Timestamp.swift │ └── pushtypes.plist └── PusherMainViewTests │ ├── DictionarySpec.swift │ ├── InterfaceFactorySpec.swift │ ├── Mocks.swift │ ├── PlistFinderSpec.swift │ ├── PusherStoreSpec.swift │ └── TimestampSpec.swift ├── README.md ├── bitrise.yml ├── fastlane └── Fastfile ├── preview-push-android-device.png ├── preview-push-ios-device.png ├── preview-push-ios-simulator.png └── pusher ├── pusher.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── pusher.xcscheme └── pusher ├── AppDelegate.swift ├── Application.xib ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Info.plist ├── main.swift └── pusher.entitlements /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | Describe the changes in this pull request. 3 | Explain your rationale of technical decisions you made (unless discussed before). 4 | 5 | ## Links 6 | Add links to github/jira issues, design documents and other relevant resources (e.g. previous discussions, platform/tool documentation etc.) 7 | 8 | # Checklist 9 | - [ ] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md) 10 | - [ ] I have added to the [changelog](../blob/master/CHANGELOG.md#Unreleased) 11 | - [ ] I wrote/updated tests for new/changed code 12 | - [ ] I removed all sensitive data **before every commit**, including API endpoints and keys 13 | - [ ] I ran `fastlane ci` without errors -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - "!*" 6 | tags: 7 | - '[0-9].[0-9]+.[0-9]+' 8 | jobs: 9 | build-zip-upload: 10 | runs-on: macOS-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: build and package 14 | run: | 15 | bundle install 16 | bundle exec fastlane ci 17 | CURRENT_TAG=${GITHUB_REF#refs/tags/} 18 | awk -v ver=$CURRENT_TAG ' 19 | /^#+ \[/ { if (p) { exit }; if ($2 == "["ver"]") { p=1; next} } p && NF 20 | ' CHANGELOG.md > artifacts/releaselog.md 21 | cd artifacts 22 | zip -9vrX PushTester.zip PushTester.app 23 | - name: create release 24 | id: create_release 25 | uses: actions/create-release@v1 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 28 | with: 29 | tag_name: ${{ github.ref }} 30 | release_name: v${{ github.ref }} Push Tester app 31 | body_path: ./artifacts/releaselog.md 32 | draft: false 33 | prerelease: false 34 | - name: upload release asset 35 | id: upload-pkg 36 | uses: actions/upload-release-asset@v1.0.2 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | upload_url: ${{ steps.create_release.outputs.upload_url }} 41 | asset_path: ./artifacts/PushTester.zip 42 | asset_name: PushTester.zip 43 | asset_content_type: application/zip -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | pull_request_target: 5 | 6 | permissions: 7 | checks: write 8 | 9 | jobs: 10 | unit-test: 11 | runs-on: macos-12 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | ref: ${{ github.event.pull_request.head.sha || github.sha }} 16 | - name: Build & Test 17 | run: | 18 | bundle install 19 | bundle exec pod install 20 | xcodebuild -workspace pusher.xcworkspace -scheme pusher -resultBundlePath TestResults test 21 | - uses: kishikawakatsumi/xcresulttool@v1 22 | with: 23 | path: TestResults.xcresult 24 | if: success() || failure() 25 | # ^ This is important because the action will be run 26 | # even if the test fails in the previous step. 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore 4 | 5 | # Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | # Various settings 10 | *.pbxuser 11 | *.xcworkspace 12 | xcuserdata/ 13 | *.xccheckout 14 | *.xcarchive 15 | 16 | # Obj-C/Swift specific 17 | *.hmap 18 | *.ipa 19 | *.dSYM.zip 20 | *.dSYM 21 | 22 | # Playgrounds 23 | timeline.xctimeline 24 | playground.xcworkspace 25 | 26 | # CocoaPods 27 | Pods/ 28 | Podfile.lock 29 | 30 | # fastlane 31 | fastlane/report.xml 32 | fastlane/Preview.html 33 | fastlane/screenshots/**/*.png 34 | fastlane/test_output 35 | Gemfile.lock 36 | fastlane/README.md 37 | *.app 38 | *.log 39 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - APNS 3 | - pusher 4 | - PusherMainView 5 | - FCM 6 | 7 | opt_in_rules: 8 | - empty_count 9 | - empty_string 10 | 11 | 12 | disabled_rules: 13 | - identifier_name 14 | - function_parameter_count 15 | - nesting 16 | - todo 17 | - blanket_disable_command 18 | 19 | line_length: 20 | warning: 150 21 | error: 200 22 | ignores_function_declarations: true 23 | ignores_comments: true 24 | ignores_urls: true 25 | 26 | function_body_length: 27 | warning: 300 28 | error: 500 29 | 30 | function_parameter_count: 31 | warning: 6 32 | error: 8 33 | 34 | type_body_length: 35 | warning: 300 36 | error: 500 37 | 38 | file_length: 39 | warning: 1000 40 | error: 1500 41 | ignore_comment_only_lines: true 42 | 43 | cyclomatic_complexity: 44 | warning: 25 45 | error: 25 46 | 47 | large_tuple: 48 | warning: 3 49 | error: 3 50 | 51 | reporter: "xcode" 52 | -------------------------------------------------------------------------------- /APNS/APNS.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'APNS' 3 | spec.version = '0.1.0' 4 | spec.license = { :type => 'MIT', :file => '../LICENSE' } 5 | spec.authors = 'Rakuten Ecosystem Mobile' 6 | spec.summary = 'APNS Framework for macOS' 7 | spec.source_files = 'APNS/*.swift' 8 | spec.framework = 'Cocoa' 9 | spec.dependency 'CupertinoJWT' 10 | spec.homepage = 'https://github.com/rakutentech/macos-push-tester' 11 | spec.source = { :git => 'https://github.com/rakutentech/macos-push-tester.git' } 12 | spec.osx.deployment_target = '10.13' 13 | spec.test_spec 'APNSTests' do |test_spec| 14 | test_spec.source_files = 'APNSTests/*.swift' 15 | test_spec.dependency 'Quick' 16 | test_spec.dependency 'Nimble' 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /APNS/APNS/APNS.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for APNS. 4 | FOUNDATION_EXPORT double APNSVersionNumber; 5 | 6 | //! Project version string for APNS. 7 | FOUNDATION_EXPORT const unsigned char APNSVersionString[]; 8 | -------------------------------------------------------------------------------- /APNS/APNS/APNSIdentity.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum APNSIdentity { 4 | public static func identities() -> [Any] { 5 | guard let kCFBooleanTrueNotNil = kCFBooleanTrue else { 6 | return [] 7 | } 8 | 9 | let query: NSDictionary = [ 10 | kSecClass: kSecClassIdentity, 11 | kSecMatchLimit: kSecMatchLimitAll, 12 | kSecReturnRef: kCFBooleanTrueNotNil 13 | ] 14 | var identities: CFTypeRef? 15 | let status: OSStatus = SecItemCopyMatching(query, &identities) 16 | 17 | guard status == noErr, 18 | let idsArray = identities as? NSArray, 19 | let result = NSMutableArray(array: idsArray) as? [SecIdentity] else { 20 | return [] 21 | } 22 | 23 | // Allow only identities with APNS certificate 24 | 25 | let filtered = result.filter { APNSSecIdentityType.type(for: $0) != .invalid }.sorted { (id1, id2) -> Bool in 26 | var cert1: SecCertificate? 27 | var cert2: SecCertificate? 28 | 29 | SecIdentityCopyCertificate(id1, &cert1) 30 | SecIdentityCopyCertificate(id2, &cert2) 31 | 32 | guard let cert1NotNil = cert1, let cert2NotNil = cert2 else { 33 | return false 34 | } 35 | 36 | guard let name1 = SecCertificateCopyShortDescription(nil, cert1NotNil, nil), 37 | let name2 = SecCertificateCopyShortDescription(nil, cert2NotNil, nil) else { 38 | return false 39 | } 40 | 41 | cert1 = nil 42 | cert2 = nil 43 | 44 | return (name1 as String).compare(name2 as String) == .orderedAscending 45 | } 46 | 47 | return filtered 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /APNS/APNS/APNSPusher.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | import CupertinoJWT 4 | 5 | public enum APNSPusherType { 6 | case none, certificate(identity: SecIdentity), token(keyID: String, teamID: String, p8: String) 7 | } 8 | 9 | public protocol APNSPushable { 10 | var type: APNSPusherType { get set } 11 | var identity: SecIdentity? { get } 12 | func pushToDevice(_ token: String, 13 | payload: [String: Any], 14 | withTopic topic: String?, 15 | priority: Int, 16 | collapseID: String?, 17 | inSandbox sandbox: Bool, 18 | pushType: String, 19 | completion: @escaping (Result) -> Void) 20 | func pushToSimulator(payload: String, 21 | appBundleID bundleID: String, 22 | completion: @escaping (Result) -> Void) 23 | } 24 | 25 | public final class APNSPusher: NSObject, APNSPushable { 26 | public var type: APNSPusherType { 27 | didSet { 28 | switch type { 29 | case .certificate(let _identity): 30 | identity = _identity 31 | session = URLSession(configuration: .ephemeral, 32 | delegate: self, 33 | delegateQueue: .main) 34 | case .token: 35 | session = URLSession(configuration: .ephemeral, 36 | delegate: nil, 37 | delegateQueue: .main) 38 | case .none: () 39 | } 40 | } 41 | } 42 | private var _identity: SecIdentity? 43 | private var session: URLSession? 44 | private var cachedProviders = Set() 45 | 46 | public private(set) var identity: SecIdentity? { 47 | get { 48 | return _identity 49 | } 50 | 51 | set(value) { 52 | if _identity != value { 53 | if _identity != nil { 54 | _identity = nil 55 | } 56 | 57 | if value != nil { 58 | _identity = value 59 | 60 | } else { 61 | _identity = nil 62 | } 63 | } 64 | } 65 | } 66 | 67 | public override init() { 68 | self.type = .none 69 | super.init() 70 | } 71 | 72 | public func pushToDevice(_ token: String, 73 | payload: [String: Any], 74 | withTopic topic: String?, 75 | priority: Int, 76 | collapseID: String?, 77 | inSandbox sandbox: Bool, 78 | pushType: String, 79 | completion: @escaping (Result) -> Void) { 80 | guard let url = URL(string: "https://api\(sandbox ? ".development" : "").push.apple.com/3/device/\(token)") else { 81 | completion(.failure(NSError(domain: "com.pusher.APNSPusher", 82 | code: 0, 83 | userInfo: [NSLocalizedDescriptionKey: "URL error"]))) 84 | return 85 | } 86 | 87 | var payload = payload 88 | if pushType == APNsPushType.liveActivity { 89 | payload["timestamp"] = Date().timeIntervalSince1970 90 | } 91 | 92 | guard let httpBody = try? JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted) else { 93 | completion(.failure(NSError(domain: "com.pusher.APNSPusher", 94 | code: 0, 95 | userInfo: [NSLocalizedDescriptionKey: "Payload error"]))) 96 | return 97 | } 98 | 99 | var request = URLRequest(url: url) 100 | 101 | request.httpMethod = "POST" 102 | 103 | request.httpBody = httpBody 104 | 105 | var correctTopic = topic 106 | 107 | request.add(pushType: pushType) 108 | 109 | if pushType == APNsPushType.liveActivity { 110 | let suffix = ".push-type.liveactivity" 111 | if !(correctTopic?.hasSuffix(suffix) ?? true) { 112 | correctTopic = correctTopic?.appending(suffix) 113 | } 114 | } 115 | 116 | if let topic = correctTopic { 117 | request.addValue(topic, forHTTPHeaderField: "apns-topic") 118 | } 119 | 120 | if let collapseID = collapseID, !collapseID.isEmpty { 121 | request.addValue(collapseID, forHTTPHeaderField: "apns-collapse-id") 122 | } 123 | 124 | request.addValue("\(priority)", forHTTPHeaderField: "apns-priority") 125 | 126 | // encode Apple Developer account as a APNs Provider Token in the authorization header 127 | if case .token(let keyID, let teamID, let p8) = type, 128 | let provider = APNSProvider(keyID: keyID, teamID: teamID, p8Digest: p8) { 129 | /// reuse same digest for up to `providerTokenTTL` as per APNs server spec 130 | if let lastProvider = cachedProviders.first(where: { $0 == provider }), lastProvider.isValid { 131 | request.addValue("bearer \(lastProvider)", forHTTPHeaderField: "authorization") 132 | } else { 133 | cachedProviders.update(with: provider) 134 | request.addValue("bearer \(provider)", forHTTPHeaderField: "authorization") 135 | } 136 | } 137 | 138 | session?.dataTask(with: request, completionHandler: { (data, response, error) in 139 | if let error = error { 140 | DispatchQueue.main.async { 141 | completion(.failure(error as NSError)) 142 | } 143 | return 144 | } 145 | 146 | guard let r = response as? HTTPURLResponse else { 147 | DispatchQueue.main.async { 148 | completion(.failure(NSError(domain: "com.pusher.APNSPusher", 149 | code: 0, 150 | userInfo: [NSLocalizedDescriptionKey: "Unknown error"]))) 151 | } 152 | return 153 | } 154 | 155 | switch r.statusCode { 156 | case 200: 157 | DispatchQueue.main.async { 158 | completion(.success(HTTPURLResponse.localizedString(forStatusCode: r.statusCode))) 159 | } 160 | 161 | default: 162 | if let data = data, 163 | let dict = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments), 164 | let json = dict as? [String: Any], 165 | let reason = json["reason"] as? String { 166 | DispatchQueue.main.async { 167 | completion(.failure(NSError(domain: "com.pusher.APNSPusher", 168 | code: r.statusCode, 169 | userInfo: [NSLocalizedDescriptionKey: reason]))) 170 | } 171 | 172 | } else { 173 | DispatchQueue.main.async { 174 | let error = NSError(domain: "com.pusher.APNSPusher", 175 | code: r.statusCode, 176 | userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: r.statusCode)]) 177 | completion(.failure(error)) 178 | } 179 | } 180 | } 181 | }).resume() 182 | } 183 | 184 | public func pushToSimulator(payload: String, appBundleID bundleID: String, completion: @escaping (Result) -> Void) { 185 | 186 | guard let payloadData = payload.data(using: .utf8), (try? JSONSerialization.jsonObject(with: payloadData)) != nil else { 187 | completion(.failure(NSError(domain: "com.pusher.APNSPusher", 188 | code: 0, 189 | userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"]))) 190 | return 191 | } 192 | 193 | let bundleCheckResult = ShellRunner.run(command: "xcrun simctl get_app_container booted \(bundleID)") 194 | if case .failure = bundleCheckResult { 195 | completion(.failure(NSError(domain: "com.pusher.APNSPusher", 196 | code: 0, 197 | userInfo: [NSLocalizedDescriptionKey: "Cannot find provided bundle ID in booted simulator"]))) 198 | return 199 | } 200 | 201 | let result = ShellRunner.run(command: "printf '\(payload)' | xcrun simctl push booted \(bundleID) -") 202 | 203 | switch result { 204 | case .failure(let error): 205 | switch error { 206 | case .commandError(let message): 207 | completion(.failure(NSError(domain: "com.pusher.APNSPusher", 208 | code: 0, 209 | userInfo: [NSLocalizedDescriptionKey: message]))) 210 | case .taskInitError(let initError): 211 | completion(.failure(NSError(domain: "com.pusher.APNSPusher", 212 | code: 0, 213 | userInfo: [NSLocalizedDescriptionKey: initError.localizedDescription]))) 214 | case .unknown: 215 | completion(.failure(NSError(domain: "com.pusher.APNSPusher", 216 | code: 0, 217 | userInfo: [NSLocalizedDescriptionKey: "Unknown error"]))) 218 | } 219 | return 220 | case .success(let message): 221 | completion(.success(message)) 222 | } 223 | } 224 | } 225 | 226 | extension APNSPusher: URLSessionDelegate { 227 | public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 228 | guard let identityNotNil = _identity else { 229 | return 230 | } 231 | var certificate: SecCertificate? 232 | 233 | SecIdentityCopyCertificate(identityNotNil, &certificate) 234 | 235 | guard let cert = certificate else { 236 | return 237 | } 238 | 239 | let cred = URLCredential(identity: identityNotNil, certificates: [cert], persistence: .forSession) 240 | 241 | certificate = nil 242 | 243 | completionHandler(.useCredential, cred) 244 | } 245 | } 246 | 247 | // MARK: - APNSProvider 248 | 249 | private struct APNSProvider { 250 | private let keyID: String 251 | private let teamID: String 252 | private let p8Digest: String 253 | private let authToken: String 254 | private let timestamp = Date() 255 | /// 20 min to resolve 256 | /// [TooManyProviderTokenUpdates](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/handling_notification_responses_from_apns) 257 | private let providerTokenTTL: TimeInterval = 60 * 20 258 | 259 | init?(keyID: String, teamID: String, p8Digest: String) { 260 | guard let authToken = try? JWT(keyID: keyID, 261 | teamID: teamID, 262 | issueDate: timestamp, 263 | expireDuration: providerTokenTTL).sign(with: p8Digest) else { 264 | return nil 265 | } 266 | self.authToken = authToken 267 | self.keyID = keyID 268 | self.teamID = teamID 269 | self.p8Digest = p8Digest 270 | } 271 | 272 | var isValid: Bool { 273 | Date().timeIntervalSince(timestamp) < providerTokenTTL 274 | } 275 | } 276 | extension APNSProvider: Equatable { 277 | static func == (lhs: Self, rhs: Self) -> Bool { 278 | lhs.keyID == rhs.keyID && lhs.teamID == rhs.teamID && lhs.p8Digest == rhs.p8Digest 279 | } 280 | } 281 | extension APNSProvider: Hashable { 282 | func hash(into hasher: inout Hasher) { 283 | hasher.combine(keyID) 284 | hasher.combine(teamID) 285 | hasher.combine(p8Digest) 286 | } 287 | } 288 | extension APNSProvider: CustomStringConvertible { 289 | var description: String { 290 | authToken 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /APNS/APNS/APNSSecIdentityType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Security 3 | 4 | enum APNSSecIdentityType: String { 5 | // http://www.apple.com/certificateauthority/Apple_WWDR_CPS 6 | case invalid = "" 7 | case development = "1.2.840.113635.100.6.3.1" 8 | case production = "1.2.840.113635.100.6.3.2" 9 | case universal = "1.2.840.113635.100.6.3.6" 10 | 11 | private static func values(for identity: SecIdentity) -> [String: Any]? { 12 | var certificate: SecCertificate? 13 | 14 | SecIdentityCopyCertificate(identity, &certificate) 15 | 16 | guard let cert = certificate else { 17 | return [:] 18 | } 19 | 20 | let keys: NSArray = [ 21 | APNSSecIdentityType.development.rawValue, 22 | APNSSecIdentityType.production.rawValue, 23 | APNSSecIdentityType.universal.rawValue 24 | ] 25 | 26 | let values: [String: Any]? = SecCertificateCopyValues(cert, keys, nil) as? [String: Any] 27 | 28 | certificate = nil 29 | 30 | return values 31 | } 32 | 33 | static func type(for identity: SecIdentity) -> APNSSecIdentityType { 34 | guard let values = values(for: identity) else { 35 | return .invalid 36 | } 37 | 38 | if (values[APNSSecIdentityType.development.rawValue] != nil) && (values[APNSSecIdentityType.production.rawValue] != nil) { 39 | return .universal 40 | 41 | } else if (values[APNSSecIdentityType.development.rawValue]) != nil { 42 | return .development 43 | 44 | } else if (values[APNSSecIdentityType.production.rawValue]) != nil { 45 | return .production 46 | 47 | } else { 48 | return .invalid 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /APNS/APNS/APNSServiceBrowser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MultipeerConnectivity 3 | 4 | public protocol APNSServiceBrowsing: AnyObject { 5 | func didUpdateDevices() 6 | } 7 | 8 | public final class APNSServiceBrowser: NSObject { 9 | public private(set) var devices: [APNSServiceDevice] 10 | private var browser: MCNearbyServiceBrowser 11 | private var peerIDToDeviceMap: [MCPeerID: APNSServiceDevice] 12 | private var _searching: Bool 13 | 14 | public weak var delegate: APNSServiceBrowsing? 15 | 16 | private enum Constants { 17 | static let defaultDeviceTokenType = "APNS" 18 | } 19 | 20 | public var searching: Bool { 21 | get { 22 | return _searching 23 | } 24 | set(value) { 25 | _searching = value 26 | 27 | if _searching { 28 | browser.startBrowsingForPeers() 29 | 30 | } else { 31 | browser.stopBrowsingForPeers() 32 | } 33 | } 34 | } 35 | 36 | public init(serviceType: String) { 37 | peerIDToDeviceMap = [:] 38 | devices = [] 39 | _searching = false 40 | let peerID = MCPeerID(displayName: Host.current().localizedName ?? "") 41 | browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType) 42 | super.init() 43 | browser.delegate = self 44 | } 45 | } 46 | 47 | extension APNSServiceBrowser: MCNearbyServiceBrowserDelegate { 48 | public func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) { 49 | guard let token = info?["token"], let appID = info?["appID"] else { 50 | return 51 | } 52 | let type = info?["type"] ?? Constants.defaultDeviceTokenType 53 | let device = APNSServiceDevice(displayName: peerID.displayName, 54 | token: token, 55 | appID: appID, 56 | type: type) 57 | DispatchQueue.main.async { 58 | self.devices.insert(device, at: self.devices.count) 59 | self.peerIDToDeviceMap[peerID] = device 60 | self.delegate?.didUpdateDevices() 61 | } 62 | } 63 | 64 | public func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { 65 | guard let lostDevice = peerIDToDeviceMap[peerID] else { 66 | return 67 | } 68 | 69 | DispatchQueue.main.async { 70 | guard let foundDevice = self.devices.firstIndex(where: { $0 == lostDevice }) else { 71 | return 72 | } 73 | 74 | self.devices.remove(at: foundDevice) 75 | self.peerIDToDeviceMap.removeValue(forKey: peerID) 76 | self.delegate?.didUpdateDevices() 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /APNS/APNS/APNSServiceDevice.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct APNSServiceDevice: Equatable { 4 | public let displayName: String 5 | public let token: String 6 | public let appID: String 7 | public let type: String 8 | } 9 | -------------------------------------------------------------------------------- /APNS/APNS/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 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2019 Rakuten. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /APNS/APNS/ShellRunner.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum ShellRunnerError: Error { 4 | case taskInitError(Error) 5 | case commandError(String) 6 | case unknown 7 | } 8 | 9 | enum ShellRunner { 10 | 11 | static func run(command: String) -> Result { 12 | let task = Process() 13 | let errorPipe = Pipe() 14 | let outputPipe = Pipe() 15 | 16 | task.standardError = errorPipe 17 | task.standardOutput = outputPipe 18 | task.arguments = ["-c", command] 19 | task.executableURL = URL(fileURLWithPath: "/bin/zsh") 20 | 21 | do { 22 | try task.run() 23 | } catch { 24 | return .failure(.taskInitError(error)) 25 | } 26 | 27 | task.waitUntilExit() 28 | 29 | guard task.terminationStatus == 0 else { 30 | let error = errorPipe.fileHandleForReading.readDataToEndOfFile() 31 | guard let resultString = String(data: error, encoding: .utf8) else { 32 | return .failure(.unknown) 33 | } 34 | return .failure(.commandError(resultString)) 35 | } 36 | 37 | let output = outputPipe.fileHandleForReading.readDataToEndOfFile() 38 | guard let resultString = String(data: output, encoding: .utf8) else { 39 | return .failure(.unknown) 40 | } 41 | return .success(resultString) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /APNS/APNS/URLRequest+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum HeaderFieldConstants { 4 | static let apnsPushType = "apns-push-type" 5 | } 6 | 7 | public enum APNsPushType { 8 | public static let liveActivity = "liveactivity" 9 | } 10 | 11 | extension URLRequest { 12 | /// Add push type for `apns-push-type` key in HTTP header only if it is not empty. 13 | mutating func add(pushType: String) { 14 | guard !pushType.isEmpty else { 15 | return 16 | } 17 | addValue(pushType, forHTTPHeaderField: HeaderFieldConstants.apnsPushType) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /APNS/APNSTests/APNSServiceDeviceSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Quick 3 | import Nimble 4 | @testable import APNS 5 | 6 | final class APNSServiceDeviceSpec: QuickSpec { 7 | override func spec() { 8 | describe("APNSServiceDeviceSpec") { 9 | it("should equal when the two instances have the same values") { 10 | let apnsServiceDevice1 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456789", appID: "com.myapp", type: "APNS") 11 | let apnsServiceDevice2 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456789", appID: "com.myapp", type: "APNS") 12 | expect(apnsServiceDevice1).to(equal(apnsServiceDevice2)) 13 | } 14 | 15 | it("should not equal when token is different") { 16 | let apnsServiceDevice1 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456789", appID: "com.myapp", type: "APNS") 17 | let apnsServiceDevice2 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456781", appID: "com.myapp", type: "APNS") 18 | expect(apnsServiceDevice1).toNot(equal(apnsServiceDevice2)) 19 | } 20 | 21 | it("should not equal when displayName is different") { 22 | let apnsServiceDevice1 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456789", appID: "com.myapp", type: "APNS") 23 | let apnsServiceDevice2 = APNSServiceDevice(displayName: "John iPhone 002", token: "0123456789", appID: "com.myapp", type: "APNS") 24 | expect(apnsServiceDevice1).toNot(equal(apnsServiceDevice2)) 25 | } 26 | 27 | it("should not equal when appID is different") { 28 | let apnsServiceDevice1 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456789", appID: "com.myapp", type: "APNS") 29 | let apnsServiceDevice2 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456789", appID: "com.myapp2", type: "APNS") 30 | expect(apnsServiceDevice1).toNot(equal(apnsServiceDevice2)) 31 | } 32 | 33 | it("should not equal when displayName and token are different") { 34 | let apnsServiceDevice1 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456789", appID: "com.myapp", type: "APNS") 35 | let apnsServiceDevice2 = APNSServiceDevice(displayName: "John iPhone 002", token: "0123456781", appID: "com.myapp", type: "APNS") 36 | expect(apnsServiceDevice1).toNot(equal(apnsServiceDevice2)) 37 | } 38 | 39 | it("should not equal when displayName and appID are different") { 40 | let apnsServiceDevice1 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456789", appID: "com.myapp", type: "APNS") 41 | let apnsServiceDevice2 = APNSServiceDevice(displayName: "John iPhone 002", token: "0123456789", appID: "com.myapp2", type: "APNS") 42 | expect(apnsServiceDevice1).toNot(equal(apnsServiceDevice2)) 43 | } 44 | 45 | it("should not equal when token and appID are different") { 46 | let apnsServiceDevice1 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456789", appID: "com.myapp", type: "APNS") 47 | let apnsServiceDevice2 = APNSServiceDevice(displayName: "John iPhone 001", token: "0123456781", appID: "com.myapp2", type: "APNS") 48 | expect(apnsServiceDevice1).toNot(equal(apnsServiceDevice2)) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /APNS/APNSTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /APNS/APNSTests/URLRequestSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Quick 3 | import Nimble 4 | @testable import APNS 5 | 6 | final class URLRequestSpec: QuickSpec { 7 | override func spec() { 8 | describe("URLRequest") { 9 | let url: URL! = URL(string: "https://test.com") 10 | var urlRequest: URLRequest! 11 | 12 | beforeEach { 13 | urlRequest = URLRequest(url: url) 14 | } 15 | 16 | context("When APNs push type is empty string value") { 17 | it("should not set apns-push-type header field") { 18 | urlRequest.add(pushType: "") 19 | 20 | let value = urlRequest.value(forHTTPHeaderField: HeaderFieldConstants.apnsPushType) 21 | expect(value).to(beNil()) 22 | } 23 | } 24 | 25 | context("When APNs push type is alert") { 26 | it("should set apns-push-type header field to alert") { 27 | urlRequest.add(pushType: "alert") 28 | 29 | let value = urlRequest.value(forHTTPHeaderField: HeaderFieldConstants.apnsPushType) 30 | expect(value).to(equal("alert")) 31 | } 32 | } 33 | 34 | context("When APNs push type is background") { 35 | it("should set apns-push-type header field to background") { 36 | urlRequest.add(pushType: "background") 37 | 38 | let value = urlRequest.value(forHTTPHeaderField: HeaderFieldConstants.apnsPushType) 39 | expect(value).to(equal("background")) 40 | } 41 | } 42 | 43 | context("When APNs push type is liveactivity") { 44 | it("should set apns-push-type header field to liveactivity") { 45 | urlRequest.add(pushType: "liveactivity") 46 | 47 | let value = urlRequest.value(forHTTPHeaderField: HeaderFieldConstants.apnsPushType) 48 | expect(value).to(equal("liveactivity")) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # The macOS Push Tester app Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## Unreleased 8 | 9 | ## [1.6.0] - 2023-07-27 10 | 11 | ### Features 12 | - Add JSON syntax coloring and validation (SDKCF-6431) 13 | - Handle properly apns-push-type by adding a push type textfield (SDKCF-6629) 14 | - Implement APNs push type selection (SDKCF-6631) 15 | 16 | ### Fixes 17 | - Fixed build issues on Xcode 14.3 18 | 19 | ## [1.5.0] - 2023-03-14 20 | 21 | ### Features 22 | - Add Live Activity support (SDKCF-6014) 23 | - Automatically update the timestamp for live activity (SDKCF-6078) 24 | - Add device token type in the devices browser (SDKCF-6079) 25 | 26 | ## [1.4.0] - 2022-11-02 27 | - Add support for Android devices (FCM) 28 | 29 | ## [1.3.0] - 2022-08-04 30 | - Add SwiftLint 31 | - PusherMainView Localization 32 | - Convert PusherStoreTests to BDD 33 | - Handle ⌘S shortcut and the Save Menu Item in the File Menu 34 | - Convert APNSTests to BDD 35 | - Replace print by RLogger.debug in the AppDelegate 36 | - Allow Undo and Redo in NSTextView with ⌘Z 37 | - Handle ⇧⌘S shortcut and the Save as... Menu Item in the File Menu 38 | - Replace pusher by PushTester in Application.xib 39 | - Show an error alert when the user tries to send an invalid JSON file 40 | - Handle ⌘O shortcut and the Open File Menu Item 41 | 42 | ## [1.2.1] - 2022-02-01 43 | - Fix TooManyProviderTokenUpdates send to device APNS error 44 | 45 | ## [1.2.0] - 2021-11-16 46 | - Add "Send push to simulator" implementation (SDKCF-4031) 47 | - Add UI for selecting push destination - device or simulator (SDKCF-4030) 48 | - GitHub Actions release on tag 49 | - Use ephemeral URL session configuration and reorder data task error messages 50 | 51 | ## [1.1.2] - 2021-07-21 52 | - improve: use the StackView in the main screen (SDKCF-3988) => Fix the json text view when there are more than 10 lines 53 | - setup Bitrise CI 54 | 55 | ## [1.1.1] - 2021-07-07 56 | - Build with Fastlane fix 57 | - Bugfix when deviceTokenTextField.string changes, the state must be updated 58 | - Rename the app 59 | 60 | ## [1.1.0] - 2021-06-22 61 | - Load a JSON File 62 | - Update README with mention of UDP service 63 | - Update README for iOS 14 info.plist Requirements 64 | - Remove code sign identity so it can be built and run on any mac 65 | - Fastlane must run even if Xcode version is not 10.3 66 | - Pusher Reducer 67 | 68 | ## [1.0.1] - 2019-12-03 69 | - Introduce newState function 70 | - Memory Leaks fixes + subcribe/unsubscribe functions in PushInteractor class 71 | - Global use of dispatch function from Push Interactor 72 | - Bugfix when the user open the Authorization Token UI 73 | - APNSSecIdentity refactoring and simplication 74 | 75 | ## [1.0.0] - 2019-11-27 76 | - Project init 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Found a bug or have a suggestion? 4 | If you find a bug in the source code or you have a suggestion for an improvement or new feature, please submit an issue to discuss with the team before working on your PR. 5 | 6 | ## Submit an Issue 7 | Please fill the following information in each (bug) issue you submit: 8 | 9 | * Title: Use a clear and descriptive title for the issue to identify the problem 10 | * Description: Description of the issue 11 | * Steps to Reproduce: numbered step by step. (1,2,3.… and so on) 12 | * Expected behaviour: What you expect to happen 13 | * Actual behaviour: What actually happens 14 | * Version: The version of the library 15 | * Repository: Link to the repository you are working with 16 | * Operating system: The operating system used 17 | * Additional information: Any additional to help to reproduce. (screenshots, animated gifs) 18 | 19 | ## Pull Requests 20 | 1. Fork the [repo](https://github.com/rakutentech/macos-push-tester.git) 21 | 2. Create a branch and implement your feature/bugfix & add test cases 22 | 3. Ensure test cases & static analysis runs succesfully 23 | 4. Submit a pull request to `master` branch 24 | 25 | Please include unit tests where necessary to cover any functionality that is introduced. 26 | 27 | ## Coding Guidelines 28 | * All features or bug fixes **must be tested** by one or more unit tests/specs 29 | * All public API methods (APNS, FCM, PusherMainView) **must be documented** in a standard format and potentially in the user guide 30 | * All code must follow the style of the existing code 31 | -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | ## Preparation 2 | 3 | Create and merge a release PR with following changes: 4 | 5 | 1. Update `MARKETING_VERSION` in `pusher/pusher.xcodeproj/project.pbxproj` 6 | 1. Add a new version entry in `CHANGELOG.md` 7 | 8 | ## Deploy 9 | 10 | ### Using CI 11 | 12 | Git tag the release commit with the format `[0-9].[0-9]+.[0-9]+` (i.e. the same `MARKETING_VERSION` value in your release PR) to kickoff a Github Actions workflow to create a release page with a build and release notes (taken from `CHANGELOG.md`). 13 | 14 | * [GitHub Actions](https://github.com/rakutentech/macos-push-tester/actions) 15 | * [Latest release page](https://github.com/rakutentech/macos-push-tester/releases/latest) 16 | * [Latest direct download link](https://github.com/rakutentech/macos-push-tester/releases/latest/download/PushTester.zip) -------------------------------------------------------------------------------- /FCM/FCM.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'FCM' 3 | spec.version = '0.1.0' 4 | spec.license = { :type => 'MIT', :file => '../LICENSE' } 5 | spec.authors = 'Rakuten Ecosystem Mobile' 6 | spec.summary = 'FCM Framework for macOS' 7 | spec.source_files = 'FCM/*.swift' 8 | spec.framework = 'Cocoa' 9 | spec.homepage = 'https://github.com/rakutentech/macos-push-tester' 10 | spec.source = { :git => 'https://github.com/rakutentech/macos-push-tester.git' } 11 | spec.osx.deployment_target = '10.13' 12 | end 13 | -------------------------------------------------------------------------------- /FCM/FCM/FCMPusher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol FCMPushable { 4 | func pushUsingLegacyEndpoint(_ token: String, 5 | payload: [String: Any], 6 | collapseID: String?, 7 | serverKey: String, 8 | completion: @escaping (Result) -> Void) 9 | func pushUsingV1Endpoint(_ token: String, 10 | payload: [String: Any], 11 | collapseID: String?, 12 | serverKey: String, 13 | projectID: String, 14 | completion: @escaping (Result) -> Void) 15 | } 16 | 17 | public final class FCMPusher: FCMPushable { 18 | private let session: URLSession 19 | 20 | public init(session: URLSession = URLSession(configuration: .ephemeral, 21 | delegate: nil, 22 | delegateQueue: .main)) { 23 | self.session = session 24 | } 25 | 26 | public func pushUsingLegacyEndpoint(_ token: String, 27 | payload: [String: Any], 28 | collapseID: String?, 29 | serverKey: String, 30 | completion: @escaping (Result) -> Void) { 31 | 32 | guard let url = URL(string: "https://fcm.googleapis.com/fcm/send") else { 33 | completion(.failure(NSError(domain: "com.pusher.FCMPusher", 34 | code: 0, 35 | userInfo: [NSLocalizedDescriptionKey: "URL error"]))) 36 | return 37 | } 38 | 39 | var updatedPayload = payload 40 | if let collapseID = collapseID, !collapseID.isEmpty { 41 | updatedPayload["collapse_key"] = collapseID 42 | } 43 | // put device token in the payload if it's not defined 44 | if updatedPayload["to"] == nil { 45 | updatedPayload["to"] = token 46 | } 47 | 48 | guard let httpBody = try? JSONSerialization.data(withJSONObject: updatedPayload, options: .prettyPrinted) else { 49 | completion(.failure(NSError(domain: "com.pusher.FCMPusher", 50 | code: 0, 51 | userInfo: [NSLocalizedDescriptionKey: "Payload error"]))) 52 | return 53 | } 54 | 55 | let request = URLRequest.fcmRequest(url: url, httpBody: httpBody, authHeaderValue: "key=\(serverKey)") 56 | 57 | session.dataTask(with: request) { data, response, error in 58 | if let error = error { 59 | DispatchQueue.main.async { 60 | completion(.failure(error as NSError)) 61 | } 62 | return 63 | } 64 | 65 | guard let r = response as? HTTPURLResponse else { 66 | DispatchQueue.main.async { 67 | completion(.failure(NSError(domain: "com.pusher.FCMPusher", 68 | code: 0, 69 | userInfo: [NSLocalizedDescriptionKey: "Unknown error"]))) 70 | } 71 | return 72 | } 73 | 74 | switch r.statusCode { 75 | case 200: 76 | guard let data = data, 77 | let responseData = try? JSONDecoder().decode(FCMLegacyResponse.self, from: data) else { 78 | DispatchQueue.main.async { 79 | completion(.failure(NSError(domain: "com.pusher.FCMPusher", 80 | code: r.statusCode, 81 | userInfo: [NSLocalizedDescriptionKey: "Cannot parse response data (200)"]))) 82 | } 83 | return 84 | } 85 | 86 | guard responseData.failure == 0 else { 87 | let results = FCMLegacyResponse.parseResults(data: data) ?? [] 88 | DispatchQueue.main.async { 89 | completion(.failure(NSError( 90 | domain: "com.pusher.FCMPusher", 91 | code: r.statusCode, 92 | userInfo: [NSLocalizedDescriptionKey: "Failures: \(responseData.failure)\n\(results)"]))) 93 | } 94 | return 95 | } 96 | 97 | DispatchQueue.main.async { 98 | completion(.success(HTTPURLResponse.localizedString(forStatusCode: r.statusCode))) 99 | } 100 | 101 | default: 102 | if let data = data, 103 | let html = String(data: data, encoding: .utf8) { 104 | DispatchQueue.main.async { 105 | completion(.failure(NSError(domain: "com.pusher.FCMPusher", 106 | code: r.statusCode, 107 | userInfo: [NSLocalizedDescriptionKey: html.slice(between: "", and: "")]))) 108 | } 109 | 110 | } else { 111 | DispatchQueue.main.async { 112 | let error = NSError(domain: "com.pusher.FCMPusher", 113 | code: r.statusCode, 114 | userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: r.statusCode)]) 115 | completion(.failure(error)) 116 | } 117 | } 118 | } 119 | }.resume() 120 | } 121 | 122 | public func pushUsingV1Endpoint(_ token: String, 123 | payload: [String: Any], 124 | collapseID: String?, 125 | serverKey: String, 126 | projectID: String, 127 | completion: @escaping (Result) -> Void) { 128 | 129 | guard let url = URL(string: "https://fcm.googleapis.com/v1/projects/\(projectID)/messages:send") else { 130 | completion(.failure(NSError(domain: "com.pusher.FCMPusher", 131 | code: 0, 132 | userInfo: [NSLocalizedDescriptionKey: "URL error"]))) 133 | return 134 | } 135 | 136 | var updatedPayload = payload 137 | if let collapseID = collapseID, !collapseID.isEmpty { 138 | updatedPayload["collapse_key"] = collapseID 139 | } 140 | // put device token in the payload if it's not defined 141 | if var messageBody = updatedPayload["message"] as? [String: Any] { 142 | if messageBody["token"] == nil { 143 | messageBody["token"] = token 144 | updatedPayload["message"] = messageBody 145 | } 146 | } 147 | 148 | guard let httpBody = try? JSONSerialization.data(withJSONObject: updatedPayload, options: .prettyPrinted) else { 149 | completion(.failure(NSError(domain: "com.pusher.FCMPusher", 150 | code: 0, 151 | userInfo: [NSLocalizedDescriptionKey: "Payload error"]))) 152 | return 153 | } 154 | 155 | let request = URLRequest.fcmRequest(url: url, httpBody: httpBody, authHeaderValue: "Bearer \(serverKey)") 156 | 157 | session.dataTask(with: request) { data, response, error in 158 | if let error = error { 159 | DispatchQueue.main.async { 160 | completion(.failure(error as NSError)) 161 | } 162 | return 163 | } 164 | 165 | guard let r = response as? HTTPURLResponse else { 166 | DispatchQueue.main.async { 167 | completion(.failure(NSError(domain: "com.pusher.FCMPusher", 168 | code: 0, 169 | userInfo: [NSLocalizedDescriptionKey: "Unknown error"]))) 170 | } 171 | return 172 | } 173 | 174 | switch r.statusCode { 175 | case 200: 176 | DispatchQueue.main.async { 177 | completion(.success(HTTPURLResponse.localizedString(forStatusCode: r.statusCode))) 178 | } 179 | 180 | default: 181 | if let data = data, 182 | let json = try? JSONDecoder().decode([String: FCMRequestError?].self, from: data), 183 | let error = json["error"] as? FCMRequestError { 184 | DispatchQueue.main.async { 185 | completion(.failure(NSError(domain: "com.pusher.FCMPusher", 186 | code: r.statusCode, 187 | userInfo: [NSLocalizedDescriptionKey: "\(error.status)\n\(error.message)"]))) 188 | } 189 | 190 | } else { 191 | DispatchQueue.main.async { 192 | let error = NSError(domain: "com.pusher.FCMPusher", 193 | code: r.statusCode, 194 | userInfo: [NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: r.statusCode)]) 195 | completion(.failure(error)) 196 | } 197 | } 198 | } 199 | }.resume() 200 | } 201 | } 202 | 203 | struct FCMRequestError: Decodable { 204 | let message: String 205 | let status: String 206 | let code: Int 207 | } 208 | 209 | struct FCMLegacyResponse: Decodable { 210 | let success: Int 211 | let failure: Int 212 | 213 | static func parseResults(data: Data) -> [[String: Any]]? { 214 | let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] 215 | return json?["results"] as? [[String: Any]] 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /FCM/FCM/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 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | © Rakuten Group, Inc. 23 | 24 | 25 | -------------------------------------------------------------------------------- /FCM/FCM/String+Slice.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | /// - Returns: a slice of text between two subtrings. 5 | /// In case one of provided strings is a not substring, the function returns the whole string. 6 | func slice(between leftBound: String, and rightBound: String) -> String { 7 | guard let leftBoundRange = range(of: leftBound), 8 | let rightBoundRange = range(of: rightBound, options: .backwards) else { 9 | return self 10 | } 11 | return String(self[leftBoundRange.upperBound.. URLRequest { 6 | var request = URLRequest(url: url) 7 | request.httpMethod = "POST" 8 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 9 | request.addValue(authHeaderValue, forHTTPHeaderField: "Authorization") 10 | request.httpBody = httpBody 11 | return request 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane", "2.201.0" 4 | gem "cocoapods" 5 | 6 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 7 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rakuten, Inc. 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. 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | workspace "pusher.xcworkspace" 2 | project "pusher/pusher" 3 | use_frameworks! 4 | platform :macos, "10.13" 5 | 6 | target 'pusher' do 7 | pod 'APNS', :path => 'APNS/', :testspecs => ['APNSTests'] 8 | pod 'FCM', :path => 'FCM/' 9 | pod 'PusherMainView', :path => 'PusherMainView/', :testspecs => ['PusherMainViewTests'] 10 | end 11 | 12 | post_install do |installer| 13 | installer.pods_project.targets.each do |target| 14 | target.build_configurations.each do |config| 15 | if config.build_settings['MACOSX_DEPLOYMENT_TARGET'].to_f < 10.11 16 | config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.11' 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Headers: -------------------------------------------------------------------------------- 1 | Versions/Current/Headers -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Highlight: -------------------------------------------------------------------------------- 1 | Versions/Current/Highlight -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Modules: -------------------------------------------------------------------------------- 1 | Versions/Current/Modules -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Resources: -------------------------------------------------------------------------------- 1 | Versions/Current/Resources -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Highlight: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/macos-push-tester/07e54808eb1d5b585f212fb04c0d8b711b9c68c0/PusherMainView/Highlight.framework/Versions/A/Highlight -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/arm64-apple-macos.private.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) 3 | // swift-module-flags: -target arm64-apple-macos10.13 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name Highlight 4 | // swift-module-flags-ignorable: -enable-bare-slash-regex 5 | import AppKit 6 | import CoreGraphics 7 | import Foundation 8 | import Swift 9 | import _Concurrency 10 | import _StringProcessing 11 | public typealias Color = AppKit.NSColor 12 | public struct DefaultJsonSyntaxHighlightingTheme : Highlight.JsonSyntaxHighlightingTheme { 13 | public init(fontSize size: CoreFoundation.CGFloat = 13) 14 | public var memberKeyColor: Highlight.Color 15 | public var whitespaceColor: Highlight.Color 16 | public var whitespaceFont: Highlight.Font 17 | public var operatorColor: Highlight.Color 18 | public var operatorFont: Highlight.Font 19 | public var numericValueColor: Highlight.Color 20 | public var numericValueFont: Highlight.Font 21 | public var stringValueColor: Highlight.Color 22 | public var stringValueFont: Highlight.Font 23 | public var literalColor: Highlight.Color 24 | public var literalFont: Highlight.Font 25 | public var unknownColor: Highlight.Color 26 | public var unknownFont: Highlight.Font 27 | } 28 | public typealias Font = AppKit.NSFont 29 | open class JsonSyntaxHighlightProvider : Highlight.SyntaxHighlightProvider { 30 | public static let shared: Highlight.JsonSyntaxHighlightProvider 31 | public init(theme: Highlight.JsonSyntaxHighlightingTheme? = nil) 32 | open var theme: Highlight.JsonSyntaxHighlightingTheme 33 | open func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax) 34 | open func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax, behaviour: Highlight.JsonTokenizerBehaviour) 35 | open func highlightJson(_ attributedText: Foundation.NSMutableAttributedString, tokens: [Highlight.JsonToken]) 36 | @objc deinit 37 | } 38 | public protocol JsonSyntaxHighlightingTheme { 39 | var whitespaceColor: Highlight.Color { get } 40 | var whitespaceFont: Highlight.Font { get } 41 | var memberKeyColor: Highlight.Color { get } 42 | var operatorColor: Highlight.Color { get } 43 | var operatorFont: Highlight.Font { get } 44 | var numericValueColor: Highlight.Color { get } 45 | var numericValueFont: Highlight.Font { get } 46 | var stringValueColor: Highlight.Color { get } 47 | var stringValueFont: Highlight.Font { get } 48 | var literalColor: Highlight.Color { get } 49 | var literalFont: Highlight.Font { get } 50 | var unknownColor: Highlight.Color { get } 51 | var unknownFont: Highlight.Font { get } 52 | } 53 | public enum JsonToken { 54 | case memberKey(Foundation.NSRange) 55 | case whitespace(Foundation.NSRange) 56 | case `operator`(Foundation.NSRange) 57 | case stringValue(Foundation.NSRange) 58 | case numericValue(Foundation.NSRange) 59 | case literal(Foundation.NSRange) 60 | case unknown(Foundation.NSRange, Highlight.JsonTokenizerError) 61 | } 62 | public struct JsonTokenizer : Highlight.Tokenizer { 63 | public typealias TToken = Highlight.JsonToken 64 | public init(behaviour: Highlight.JsonTokenizerBehaviour) 65 | public var behaviour: Highlight.JsonTokenizerBehaviour 66 | public func tokenize(_ text: Swift.String) -> [Highlight.JsonToken] 67 | } 68 | public enum JsonTokenizerBehaviour { 69 | case strict 70 | case lenient 71 | public static func == (a: Highlight.JsonTokenizerBehaviour, b: Highlight.JsonTokenizerBehaviour) -> Swift.Bool 72 | public func hash(into hasher: inout Swift.Hasher) 73 | public var hashValue: Swift.Int { 74 | get 75 | } 76 | } 77 | public enum JsonTokenizerError : Swift.Error { 78 | case invalidSymbol(expected: Swift.Character?, actual: Swift.Character?) 79 | case expectedSymbol 80 | case unexpectedSymbol(description: Swift.String) 81 | case unenclosedQuotationMarks 82 | case invalidProperty 83 | } 84 | public enum Syntax { 85 | case json 86 | case other(identifier: Swift.String) 87 | } 88 | public protocol SyntaxHighlightProvider { 89 | func highlight(_ text: Swift.String, as syntax: Highlight.Syntax) -> Foundation.NSAttributedString 90 | func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax) 91 | } 92 | extension Highlight.SyntaxHighlightProvider { 93 | public func highlight(_ text: Swift.String, as syntax: Highlight.Syntax) -> Foundation.NSAttributedString 94 | } 95 | public protocol Tokenizer { 96 | associatedtype TToken 97 | func tokenize(_ text: Swift.String) -> [Self.TToken] 98 | } 99 | extension Highlight.JsonTokenizerBehaviour : Swift.Equatable {} 100 | extension Highlight.JsonTokenizerBehaviour : Swift.Hashable {} 101 | -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/arm64-apple-macos.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/macos-push-tester/07e54808eb1d5b585f212fb04c0d8b711b9c68c0/PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/arm64-apple-macos.swiftdoc -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/arm64-apple-macos.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) 3 | // swift-module-flags: -target arm64-apple-macos10.13 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name Highlight 4 | // swift-module-flags-ignorable: -enable-bare-slash-regex 5 | import AppKit 6 | import CoreGraphics 7 | import Foundation 8 | import Swift 9 | import _Concurrency 10 | import _StringProcessing 11 | public typealias Color = AppKit.NSColor 12 | public struct DefaultJsonSyntaxHighlightingTheme : Highlight.JsonSyntaxHighlightingTheme { 13 | public init(fontSize size: CoreFoundation.CGFloat = 13) 14 | public var memberKeyColor: Highlight.Color 15 | public var whitespaceColor: Highlight.Color 16 | public var whitespaceFont: Highlight.Font 17 | public var operatorColor: Highlight.Color 18 | public var operatorFont: Highlight.Font 19 | public var numericValueColor: Highlight.Color 20 | public var numericValueFont: Highlight.Font 21 | public var stringValueColor: Highlight.Color 22 | public var stringValueFont: Highlight.Font 23 | public var literalColor: Highlight.Color 24 | public var literalFont: Highlight.Font 25 | public var unknownColor: Highlight.Color 26 | public var unknownFont: Highlight.Font 27 | } 28 | public typealias Font = AppKit.NSFont 29 | open class JsonSyntaxHighlightProvider : Highlight.SyntaxHighlightProvider { 30 | public static let shared: Highlight.JsonSyntaxHighlightProvider 31 | public init(theme: Highlight.JsonSyntaxHighlightingTheme? = nil) 32 | open var theme: Highlight.JsonSyntaxHighlightingTheme 33 | open func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax) 34 | open func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax, behaviour: Highlight.JsonTokenizerBehaviour) 35 | open func highlightJson(_ attributedText: Foundation.NSMutableAttributedString, tokens: [Highlight.JsonToken]) 36 | @objc deinit 37 | } 38 | public protocol JsonSyntaxHighlightingTheme { 39 | var whitespaceColor: Highlight.Color { get } 40 | var whitespaceFont: Highlight.Font { get } 41 | var memberKeyColor: Highlight.Color { get } 42 | var operatorColor: Highlight.Color { get } 43 | var operatorFont: Highlight.Font { get } 44 | var numericValueColor: Highlight.Color { get } 45 | var numericValueFont: Highlight.Font { get } 46 | var stringValueColor: Highlight.Color { get } 47 | var stringValueFont: Highlight.Font { get } 48 | var literalColor: Highlight.Color { get } 49 | var literalFont: Highlight.Font { get } 50 | var unknownColor: Highlight.Color { get } 51 | var unknownFont: Highlight.Font { get } 52 | } 53 | public enum JsonToken { 54 | case memberKey(Foundation.NSRange) 55 | case whitespace(Foundation.NSRange) 56 | case `operator`(Foundation.NSRange) 57 | case stringValue(Foundation.NSRange) 58 | case numericValue(Foundation.NSRange) 59 | case literal(Foundation.NSRange) 60 | case unknown(Foundation.NSRange, Highlight.JsonTokenizerError) 61 | } 62 | public struct JsonTokenizer : Highlight.Tokenizer { 63 | public typealias TToken = Highlight.JsonToken 64 | public init(behaviour: Highlight.JsonTokenizerBehaviour) 65 | public var behaviour: Highlight.JsonTokenizerBehaviour 66 | public func tokenize(_ text: Swift.String) -> [Highlight.JsonToken] 67 | } 68 | public enum JsonTokenizerBehaviour { 69 | case strict 70 | case lenient 71 | public static func == (a: Highlight.JsonTokenizerBehaviour, b: Highlight.JsonTokenizerBehaviour) -> Swift.Bool 72 | public func hash(into hasher: inout Swift.Hasher) 73 | public var hashValue: Swift.Int { 74 | get 75 | } 76 | } 77 | public enum JsonTokenizerError : Swift.Error { 78 | case invalidSymbol(expected: Swift.Character?, actual: Swift.Character?) 79 | case expectedSymbol 80 | case unexpectedSymbol(description: Swift.String) 81 | case unenclosedQuotationMarks 82 | case invalidProperty 83 | } 84 | public enum Syntax { 85 | case json 86 | case other(identifier: Swift.String) 87 | } 88 | public protocol SyntaxHighlightProvider { 89 | func highlight(_ text: Swift.String, as syntax: Highlight.Syntax) -> Foundation.NSAttributedString 90 | func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax) 91 | } 92 | extension Highlight.SyntaxHighlightProvider { 93 | public func highlight(_ text: Swift.String, as syntax: Highlight.Syntax) -> Foundation.NSAttributedString 94 | } 95 | public protocol Tokenizer { 96 | associatedtype TToken 97 | func tokenize(_ text: Swift.String) -> [Self.TToken] 98 | } 99 | extension Highlight.JsonTokenizerBehaviour : Swift.Equatable {} 100 | extension Highlight.JsonTokenizerBehaviour : Swift.Hashable {} 101 | -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/arm64-apple-macos.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/macos-push-tester/07e54808eb1d5b585f212fb04c0d8b711b9c68c0/PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/arm64-apple-macos.swiftmodule -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/x86_64-apple-macos.private.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) 3 | // swift-module-flags: -target x86_64-apple-macos10.13 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name Highlight 4 | // swift-module-flags-ignorable: -enable-bare-slash-regex 5 | import AppKit 6 | import CoreGraphics 7 | import Foundation 8 | import Swift 9 | import _Concurrency 10 | import _StringProcessing 11 | public typealias Color = AppKit.NSColor 12 | public struct DefaultJsonSyntaxHighlightingTheme : Highlight.JsonSyntaxHighlightingTheme { 13 | public init(fontSize size: CoreFoundation.CGFloat = 13) 14 | public var memberKeyColor: Highlight.Color 15 | public var whitespaceColor: Highlight.Color 16 | public var whitespaceFont: Highlight.Font 17 | public var operatorColor: Highlight.Color 18 | public var operatorFont: Highlight.Font 19 | public var numericValueColor: Highlight.Color 20 | public var numericValueFont: Highlight.Font 21 | public var stringValueColor: Highlight.Color 22 | public var stringValueFont: Highlight.Font 23 | public var literalColor: Highlight.Color 24 | public var literalFont: Highlight.Font 25 | public var unknownColor: Highlight.Color 26 | public var unknownFont: Highlight.Font 27 | } 28 | public typealias Font = AppKit.NSFont 29 | open class JsonSyntaxHighlightProvider : Highlight.SyntaxHighlightProvider { 30 | public static let shared: Highlight.JsonSyntaxHighlightProvider 31 | public init(theme: Highlight.JsonSyntaxHighlightingTheme? = nil) 32 | open var theme: Highlight.JsonSyntaxHighlightingTheme 33 | open func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax) 34 | open func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax, behaviour: Highlight.JsonTokenizerBehaviour) 35 | open func highlightJson(_ attributedText: Foundation.NSMutableAttributedString, tokens: [Highlight.JsonToken]) 36 | @objc deinit 37 | } 38 | public protocol JsonSyntaxHighlightingTheme { 39 | var whitespaceColor: Highlight.Color { get } 40 | var whitespaceFont: Highlight.Font { get } 41 | var memberKeyColor: Highlight.Color { get } 42 | var operatorColor: Highlight.Color { get } 43 | var operatorFont: Highlight.Font { get } 44 | var numericValueColor: Highlight.Color { get } 45 | var numericValueFont: Highlight.Font { get } 46 | var stringValueColor: Highlight.Color { get } 47 | var stringValueFont: Highlight.Font { get } 48 | var literalColor: Highlight.Color { get } 49 | var literalFont: Highlight.Font { get } 50 | var unknownColor: Highlight.Color { get } 51 | var unknownFont: Highlight.Font { get } 52 | } 53 | public enum JsonToken { 54 | case memberKey(Foundation.NSRange) 55 | case whitespace(Foundation.NSRange) 56 | case `operator`(Foundation.NSRange) 57 | case stringValue(Foundation.NSRange) 58 | case numericValue(Foundation.NSRange) 59 | case literal(Foundation.NSRange) 60 | case unknown(Foundation.NSRange, Highlight.JsonTokenizerError) 61 | } 62 | public struct JsonTokenizer : Highlight.Tokenizer { 63 | public typealias TToken = Highlight.JsonToken 64 | public init(behaviour: Highlight.JsonTokenizerBehaviour) 65 | public var behaviour: Highlight.JsonTokenizerBehaviour 66 | public func tokenize(_ text: Swift.String) -> [Highlight.JsonToken] 67 | } 68 | public enum JsonTokenizerBehaviour { 69 | case strict 70 | case lenient 71 | public static func == (a: Highlight.JsonTokenizerBehaviour, b: Highlight.JsonTokenizerBehaviour) -> Swift.Bool 72 | public func hash(into hasher: inout Swift.Hasher) 73 | public var hashValue: Swift.Int { 74 | get 75 | } 76 | } 77 | public enum JsonTokenizerError : Swift.Error { 78 | case invalidSymbol(expected: Swift.Character?, actual: Swift.Character?) 79 | case expectedSymbol 80 | case unexpectedSymbol(description: Swift.String) 81 | case unenclosedQuotationMarks 82 | case invalidProperty 83 | } 84 | public enum Syntax { 85 | case json 86 | case other(identifier: Swift.String) 87 | } 88 | public protocol SyntaxHighlightProvider { 89 | func highlight(_ text: Swift.String, as syntax: Highlight.Syntax) -> Foundation.NSAttributedString 90 | func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax) 91 | } 92 | extension Highlight.SyntaxHighlightProvider { 93 | public func highlight(_ text: Swift.String, as syntax: Highlight.Syntax) -> Foundation.NSAttributedString 94 | } 95 | public protocol Tokenizer { 96 | associatedtype TToken 97 | func tokenize(_ text: Swift.String) -> [Self.TToken] 98 | } 99 | extension Highlight.JsonTokenizerBehaviour : Swift.Equatable {} 100 | extension Highlight.JsonTokenizerBehaviour : Swift.Hashable {} 101 | -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/x86_64-apple-macos.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/macos-push-tester/07e54808eb1d5b585f212fb04c0d8b711b9c68c0/PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/x86_64-apple-macos.swiftdoc -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/x86_64-apple-macos.swiftinterface: -------------------------------------------------------------------------------- 1 | // swift-interface-format-version: 1.0 2 | // swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51) 3 | // swift-module-flags: -target x86_64-apple-macos10.13 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name Highlight 4 | // swift-module-flags-ignorable: -enable-bare-slash-regex 5 | import AppKit 6 | import CoreGraphics 7 | import Foundation 8 | import Swift 9 | import _Concurrency 10 | import _StringProcessing 11 | public typealias Color = AppKit.NSColor 12 | public struct DefaultJsonSyntaxHighlightingTheme : Highlight.JsonSyntaxHighlightingTheme { 13 | public init(fontSize size: CoreFoundation.CGFloat = 13) 14 | public var memberKeyColor: Highlight.Color 15 | public var whitespaceColor: Highlight.Color 16 | public var whitespaceFont: Highlight.Font 17 | public var operatorColor: Highlight.Color 18 | public var operatorFont: Highlight.Font 19 | public var numericValueColor: Highlight.Color 20 | public var numericValueFont: Highlight.Font 21 | public var stringValueColor: Highlight.Color 22 | public var stringValueFont: Highlight.Font 23 | public var literalColor: Highlight.Color 24 | public var literalFont: Highlight.Font 25 | public var unknownColor: Highlight.Color 26 | public var unknownFont: Highlight.Font 27 | } 28 | public typealias Font = AppKit.NSFont 29 | open class JsonSyntaxHighlightProvider : Highlight.SyntaxHighlightProvider { 30 | public static let shared: Highlight.JsonSyntaxHighlightProvider 31 | public init(theme: Highlight.JsonSyntaxHighlightingTheme? = nil) 32 | open var theme: Highlight.JsonSyntaxHighlightingTheme 33 | open func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax) 34 | open func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax, behaviour: Highlight.JsonTokenizerBehaviour) 35 | open func highlightJson(_ attributedText: Foundation.NSMutableAttributedString, tokens: [Highlight.JsonToken]) 36 | @objc deinit 37 | } 38 | public protocol JsonSyntaxHighlightingTheme { 39 | var whitespaceColor: Highlight.Color { get } 40 | var whitespaceFont: Highlight.Font { get } 41 | var memberKeyColor: Highlight.Color { get } 42 | var operatorColor: Highlight.Color { get } 43 | var operatorFont: Highlight.Font { get } 44 | var numericValueColor: Highlight.Color { get } 45 | var numericValueFont: Highlight.Font { get } 46 | var stringValueColor: Highlight.Color { get } 47 | var stringValueFont: Highlight.Font { get } 48 | var literalColor: Highlight.Color { get } 49 | var literalFont: Highlight.Font { get } 50 | var unknownColor: Highlight.Color { get } 51 | var unknownFont: Highlight.Font { get } 52 | } 53 | public enum JsonToken { 54 | case memberKey(Foundation.NSRange) 55 | case whitespace(Foundation.NSRange) 56 | case `operator`(Foundation.NSRange) 57 | case stringValue(Foundation.NSRange) 58 | case numericValue(Foundation.NSRange) 59 | case literal(Foundation.NSRange) 60 | case unknown(Foundation.NSRange, Highlight.JsonTokenizerError) 61 | } 62 | public struct JsonTokenizer : Highlight.Tokenizer { 63 | public typealias TToken = Highlight.JsonToken 64 | public init(behaviour: Highlight.JsonTokenizerBehaviour) 65 | public var behaviour: Highlight.JsonTokenizerBehaviour 66 | public func tokenize(_ text: Swift.String) -> [Highlight.JsonToken] 67 | } 68 | public enum JsonTokenizerBehaviour { 69 | case strict 70 | case lenient 71 | public static func == (a: Highlight.JsonTokenizerBehaviour, b: Highlight.JsonTokenizerBehaviour) -> Swift.Bool 72 | public func hash(into hasher: inout Swift.Hasher) 73 | public var hashValue: Swift.Int { 74 | get 75 | } 76 | } 77 | public enum JsonTokenizerError : Swift.Error { 78 | case invalidSymbol(expected: Swift.Character?, actual: Swift.Character?) 79 | case expectedSymbol 80 | case unexpectedSymbol(description: Swift.String) 81 | case unenclosedQuotationMarks 82 | case invalidProperty 83 | } 84 | public enum Syntax { 85 | case json 86 | case other(identifier: Swift.String) 87 | } 88 | public protocol SyntaxHighlightProvider { 89 | func highlight(_ text: Swift.String, as syntax: Highlight.Syntax) -> Foundation.NSAttributedString 90 | func highlight(_ attributedText: Foundation.NSMutableAttributedString, as syntax: Highlight.Syntax) 91 | } 92 | extension Highlight.SyntaxHighlightProvider { 93 | public func highlight(_ text: Swift.String, as syntax: Highlight.Syntax) -> Foundation.NSAttributedString 94 | } 95 | public protocol Tokenizer { 96 | associatedtype TToken 97 | func tokenize(_ text: Swift.String) -> [Self.TToken] 98 | } 99 | extension Highlight.JsonTokenizerBehaviour : Swift.Equatable {} 100 | extension Highlight.JsonTokenizerBehaviour : Swift.Hashable {} 101 | -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/x86_64-apple-macos.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/macos-push-tester/07e54808eb1d5b585f212fb04c0d8b711b9c68c0/PusherMainView/Highlight.framework/Versions/A/Modules/Highlight.swiftmodule/x86_64-apple-macos.swiftmodule -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module Highlight { 2 | header "Highlight-Swift.h" 3 | requires objc 4 | } 5 | -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 22D68 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | Highlight 11 | CFBundleIdentifier 12 | Highlight 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Highlight 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | 0.4.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleSupportedPlatforms 24 | 25 | MacOSX 26 | 27 | CFBundleVersion 28 | 1 29 | DTCompiler 30 | com.apple.compilers.llvm.clang.1_0 31 | DTPlatformBuild 32 | 14C18 33 | DTPlatformName 34 | macosx 35 | DTPlatformVersion 36 | 13.1 37 | DTSDKBuild 38 | 22C55 39 | DTSDKName 40 | macosx13.1 41 | DTXcode 42 | 1420 43 | DTXcodeBuild 44 | 14C18 45 | LSMinimumSystemVersion 46 | 10.13 47 | 48 | 49 | -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/A/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Resources/Info.plist 8 | 9 | eiEIHkC/o+WnIqUthSJ4wllvmjY= 10 | 11 | 12 | files2 13 | 14 | Headers/Highlight-Swift.h 15 | 16 | hash2 17 | 18 | nyrplbKSvWYNVrwROeBSKfzHgqjQVCb0OpYI4ZVo3uU= 19 | 20 | 21 | Modules/Highlight.swiftmodule/arm64-apple-macos.abi.json 22 | 23 | hash2 24 | 25 | 0SU2eYAe8rcU4xim1Zfb++FAkhFK+kVY3ZNM6EXedE8= 26 | 27 | 28 | Modules/Highlight.swiftmodule/arm64-apple-macos.private.swiftinterface 29 | 30 | hash2 31 | 32 | iWgrjZSwv0T6gx2CyPeT9XaPSL8FyG3iNJf+wVZkG+g= 33 | 34 | 35 | Modules/Highlight.swiftmodule/arm64-apple-macos.swiftdoc 36 | 37 | hash2 38 | 39 | S7lPdKrSYXg8UIW6QaMg7fkEusTKL2oBlCvD9AOG/RY= 40 | 41 | 42 | Modules/Highlight.swiftmodule/arm64-apple-macos.swiftinterface 43 | 44 | hash2 45 | 46 | iWgrjZSwv0T6gx2CyPeT9XaPSL8FyG3iNJf+wVZkG+g= 47 | 48 | 49 | Modules/Highlight.swiftmodule/arm64-apple-macos.swiftmodule 50 | 51 | hash2 52 | 53 | v3LELHvbxOSu+lGkK4uuC0iD90Vt1pQLILRYZF/r0A8= 54 | 55 | 56 | Modules/Highlight.swiftmodule/x86_64-apple-macos.abi.json 57 | 58 | hash2 59 | 60 | 0SU2eYAe8rcU4xim1Zfb++FAkhFK+kVY3ZNM6EXedE8= 61 | 62 | 63 | Modules/Highlight.swiftmodule/x86_64-apple-macos.private.swiftinterface 64 | 65 | hash2 66 | 67 | RVhbldnL1/Fdw0xW36L44zOmZokEtAwVhqJNCraPka8= 68 | 69 | 70 | Modules/Highlight.swiftmodule/x86_64-apple-macos.swiftdoc 71 | 72 | hash2 73 | 74 | I/gf+vw5sOjjHTeVfnZ7z2tfJ83SwhP9xLowgIRBdts= 75 | 76 | 77 | Modules/Highlight.swiftmodule/x86_64-apple-macos.swiftinterface 78 | 79 | hash2 80 | 81 | RVhbldnL1/Fdw0xW36L44zOmZokEtAwVhqJNCraPka8= 82 | 83 | 84 | Modules/Highlight.swiftmodule/x86_64-apple-macos.swiftmodule 85 | 86 | hash2 87 | 88 | gD7FUEqr5I53LcpK6gRIG4MO1KsWaAdsbt6KRWuUjVY= 89 | 90 | 91 | Modules/module.modulemap 92 | 93 | hash2 94 | 95 | v17pmLEKedYwfJF89EjiZNnP27ipZErGZ2gpZQq/ox8= 96 | 97 | 98 | Resources/Info.plist 99 | 100 | hash2 101 | 102 | NUImU/aMqyq+cj6bFY0/MiKBL5B3cHBIPPYbiJhcR40= 103 | 104 | 105 | 106 | rules 107 | 108 | ^Resources/ 109 | 110 | ^Resources/.*\.lproj/ 111 | 112 | optional 113 | 114 | weight 115 | 1000 116 | 117 | ^Resources/.*\.lproj/locversion.plist$ 118 | 119 | omit 120 | 121 | weight 122 | 1100 123 | 124 | ^Resources/Base\.lproj/ 125 | 126 | weight 127 | 1010 128 | 129 | ^version.plist$ 130 | 131 | 132 | rules2 133 | 134 | .*\.dSYM($|/) 135 | 136 | weight 137 | 11 138 | 139 | ^(.*/)?\.DS_Store$ 140 | 141 | omit 142 | 143 | weight 144 | 2000 145 | 146 | ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ 147 | 148 | nested 149 | 150 | weight 151 | 10 152 | 153 | ^.* 154 | 155 | ^Info\.plist$ 156 | 157 | omit 158 | 159 | weight 160 | 20 161 | 162 | ^PkgInfo$ 163 | 164 | omit 165 | 166 | weight 167 | 20 168 | 169 | ^Resources/ 170 | 171 | weight 172 | 20 173 | 174 | ^Resources/.*\.lproj/ 175 | 176 | optional 177 | 178 | weight 179 | 1000 180 | 181 | ^Resources/.*\.lproj/locversion.plist$ 182 | 183 | omit 184 | 185 | weight 186 | 1100 187 | 188 | ^Resources/Base\.lproj/ 189 | 190 | weight 191 | 1010 192 | 193 | ^[^/]+$ 194 | 195 | nested 196 | 197 | weight 198 | 10 199 | 200 | ^embedded\.provisionprofile$ 201 | 202 | weight 203 | 20 204 | 205 | ^version\.plist$ 206 | 207 | weight 208 | 20 209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /PusherMainView/Highlight.framework/Versions/Current: -------------------------------------------------------------------------------- 1 | A -------------------------------------------------------------------------------- /PusherMainView/PusherMainView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'PusherMainView' 3 | spec.version = '0.1.0' 4 | spec.license = { :type => 'MIT', :file => '../LICENSE' } 5 | spec.authors = 'Rakuten Ecosystem Mobile' 6 | spec.summary = 'PusherMainView Framework for macOS' 7 | spec.source_files = 'PusherMainView/*.swift' 8 | spec.resource_bundles = { 'PusherMainViewResources' => ['PusherMainView/*.strings'] } 9 | spec.framework = 'Cocoa' 10 | spec.dependency 'APNS' 11 | spec.dependency 'FCM' 12 | spec.vendored_framework = 'Highlight.framework' 13 | spec.homepage = 'https://github.com/rakutentech/macos-push-tester' 14 | spec.source = { :git => 'https://github.com/rakutentech/macos-push-tester.git' } 15 | spec.osx.deployment_target = '10.13' 16 | spec.resources = ["PusherMainView/Base.lproj/Pusher.storyboard","PusherMainView/MainPlayground.playground","PusherMainView/pushtypes.plist"] 17 | spec.test_spec 'PusherMainViewTests' do |test_spec| 18 | test_spec.source_files = 'PusherMainViewTests/*.swift' 19 | test_spec.dependency 'Quick' 20 | test_spec.dependency 'Nimble' 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3A590CE823261E7C005AA8BA /* DevicesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A590CE723261E7C005AA8BA /* DevicesViewController.swift */; }; 11 | 3A5BE7F62328BC050072CA50 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5BE7F52328BC050072CA50 /* Router.swift */; }; 12 | 3AA665D423223E220011F310 /* PusherMainView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3AA665D223223E220011F310 /* PusherMainView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13 | 3AA665DF23223E810011F310 /* PusherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA665DA23223E810011F310 /* PusherViewController.swift */; }; 14 | 3AA665E323223E8B0011F310 /* Pusher.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3AA665E123223E8A0011F310 /* Pusher.storyboard */; }; 15 | 3AA665E623223E990011F310 /* APNS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AA665E523223E990011F310 /* APNS.framework */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 3A590CE723261E7C005AA8BA /* DevicesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewController.swift; sourceTree = ""; }; 20 | 3A5BE7F52328BC050072CA50 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; 21 | 3AA665CF23223E220011F310 /* PusherMainView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PusherMainView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 3AA665D223223E220011F310 /* PusherMainView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PusherMainView.h; sourceTree = ""; }; 23 | 3AA665D323223E220011F310 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | 3AA665DA23223E810011F310 /* PusherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PusherViewController.swift; sourceTree = ""; }; 25 | 3AA665DE23223E810011F310 /* MainPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = MainPlayground.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 26 | 3AA665E223223E8A0011F310 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Pusher.storyboard; sourceTree = ""; }; 27 | 3AA665E523223E990011F310 /* APNS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = APNS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | 3AA665CC23223E220011F310 /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | 3AA665E623223E990011F310 /* APNS.framework in Frameworks */, 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 3AA665C523223E210011F310 = { 43 | isa = PBXGroup; 44 | children = ( 45 | 3AA665D123223E220011F310 /* PusherMainView */, 46 | 3AA665D023223E220011F310 /* Products */, 47 | 3AA665E423223E990011F310 /* Frameworks */, 48 | ); 49 | sourceTree = ""; 50 | }; 51 | 3AA665D023223E220011F310 /* Products */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | 3AA665CF23223E220011F310 /* PusherMainView.framework */, 55 | ); 56 | name = Products; 57 | sourceTree = ""; 58 | }; 59 | 3AA665D123223E220011F310 /* PusherMainView */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 3A590CE723261E7C005AA8BA /* DevicesViewController.swift */, 63 | 3AA665D323223E220011F310 /* Info.plist */, 64 | 3AA665DE23223E810011F310 /* MainPlayground.playground */, 65 | 3AA665E123223E8A0011F310 /* Pusher.storyboard */, 66 | 3AA665D223223E220011F310 /* PusherMainView.h */, 67 | 3AA665DA23223E810011F310 /* PusherViewController.swift */, 68 | 3A5BE7F52328BC050072CA50 /* Router.swift */, 69 | ); 70 | path = PusherMainView; 71 | sourceTree = ""; 72 | }; 73 | 3AA665E423223E990011F310 /* Frameworks */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 3AA665E523223E990011F310 /* APNS.framework */, 77 | ); 78 | name = Frameworks; 79 | sourceTree = ""; 80 | }; 81 | /* End PBXGroup section */ 82 | 83 | /* Begin PBXHeadersBuildPhase section */ 84 | 3AA665CA23223E220011F310 /* Headers */ = { 85 | isa = PBXHeadersBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | 3AA665D423223E220011F310 /* PusherMainView.h in Headers */, 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | /* End PBXHeadersBuildPhase section */ 93 | 94 | /* Begin PBXNativeTarget section */ 95 | 3AA665CE23223E220011F310 /* PusherMainView */ = { 96 | isa = PBXNativeTarget; 97 | buildConfigurationList = 3AA665D723223E220011F310 /* Build configuration list for PBXNativeTarget "PusherMainView" */; 98 | buildPhases = ( 99 | 3AA665CA23223E220011F310 /* Headers */, 100 | 3AA665CB23223E220011F310 /* Sources */, 101 | 3AA665CC23223E220011F310 /* Frameworks */, 102 | 3AA665CD23223E220011F310 /* Resources */, 103 | ); 104 | buildRules = ( 105 | ); 106 | dependencies = ( 107 | ); 108 | name = PusherMainView; 109 | productName = PusherMainView; 110 | productReference = 3AA665CF23223E220011F310 /* PusherMainView.framework */; 111 | productType = "com.apple.product-type.framework"; 112 | }; 113 | /* End PBXNativeTarget section */ 114 | 115 | /* Begin PBXProject section */ 116 | 3AA665C623223E210011F310 /* Project object */ = { 117 | isa = PBXProject; 118 | attributes = { 119 | LastUpgradeCheck = 1030; 120 | ORGANIZATIONNAME = Rakuten; 121 | TargetAttributes = { 122 | 3AA665CE23223E220011F310 = { 123 | CreatedOnToolsVersion = 10.3; 124 | LastSwiftMigration = 1030; 125 | }; 126 | }; 127 | }; 128 | buildConfigurationList = 3AA665C923223E210011F310 /* Build configuration list for PBXProject "PusherMainView" */; 129 | compatibilityVersion = "Xcode 9.3"; 130 | developmentRegion = en; 131 | hasScannedForEncodings = 0; 132 | knownRegions = ( 133 | en, 134 | Base, 135 | ); 136 | mainGroup = 3AA665C523223E210011F310; 137 | productRefGroup = 3AA665D023223E220011F310 /* Products */; 138 | projectDirPath = ""; 139 | projectRoot = ""; 140 | targets = ( 141 | 3AA665CE23223E220011F310 /* PusherMainView */, 142 | ); 143 | }; 144 | /* End PBXProject section */ 145 | 146 | /* Begin PBXResourcesBuildPhase section */ 147 | 3AA665CD23223E220011F310 /* Resources */ = { 148 | isa = PBXResourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | 3AA665E323223E8B0011F310 /* Pusher.storyboard in Resources */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXResourcesBuildPhase section */ 156 | 157 | /* Begin PBXSourcesBuildPhase section */ 158 | 3AA665CB23223E220011F310 /* Sources */ = { 159 | isa = PBXSourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 3A590CE823261E7C005AA8BA /* DevicesViewController.swift in Sources */, 163 | 3A5BE7F62328BC050072CA50 /* Router.swift in Sources */, 164 | 3AA665DF23223E810011F310 /* PusherViewController.swift in Sources */, 165 | ); 166 | runOnlyForDeploymentPostprocessing = 0; 167 | }; 168 | /* End PBXSourcesBuildPhase section */ 169 | 170 | /* Begin PBXVariantGroup section */ 171 | 3AA665E123223E8A0011F310 /* Pusher.storyboard */ = { 172 | isa = PBXVariantGroup; 173 | children = ( 174 | 3AA665E223223E8A0011F310 /* Base */, 175 | ); 176 | name = Pusher.storyboard; 177 | sourceTree = ""; 178 | }; 179 | /* End PBXVariantGroup section */ 180 | 181 | /* Begin XCBuildConfiguration section */ 182 | 3AA665D523223E220011F310 /* Debug */ = { 183 | isa = XCBuildConfiguration; 184 | buildSettings = { 185 | ALWAYS_SEARCH_USER_PATHS = NO; 186 | CLANG_ANALYZER_NONNULL = YES; 187 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 188 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 189 | CLANG_CXX_LIBRARY = "libc++"; 190 | CLANG_ENABLE_MODULES = YES; 191 | CLANG_ENABLE_OBJC_ARC = YES; 192 | CLANG_ENABLE_OBJC_WEAK = YES; 193 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 194 | CLANG_WARN_BOOL_CONVERSION = YES; 195 | CLANG_WARN_COMMA = YES; 196 | CLANG_WARN_CONSTANT_CONVERSION = YES; 197 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 198 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 199 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 200 | CLANG_WARN_EMPTY_BODY = YES; 201 | CLANG_WARN_ENUM_CONVERSION = YES; 202 | CLANG_WARN_INFINITE_RECURSION = YES; 203 | CLANG_WARN_INT_CONVERSION = YES; 204 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 205 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 206 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 207 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 208 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 209 | CLANG_WARN_STRICT_PROTOTYPES = YES; 210 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 211 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 212 | CLANG_WARN_UNREACHABLE_CODE = YES; 213 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 214 | CODE_SIGN_IDENTITY = "Mac Developer"; 215 | COPY_PHASE_STRIP = NO; 216 | CURRENT_PROJECT_VERSION = 1; 217 | DEBUG_INFORMATION_FORMAT = dwarf; 218 | ENABLE_STRICT_OBJC_MSGSEND = YES; 219 | ENABLE_TESTABILITY = YES; 220 | GCC_C_LANGUAGE_STANDARD = gnu11; 221 | GCC_DYNAMIC_NO_PIC = NO; 222 | GCC_NO_COMMON_BLOCKS = YES; 223 | GCC_OPTIMIZATION_LEVEL = 0; 224 | GCC_PREPROCESSOR_DEFINITIONS = ( 225 | "DEBUG=1", 226 | "$(inherited)", 227 | ); 228 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 229 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 230 | GCC_WARN_UNDECLARED_SELECTOR = YES; 231 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 232 | GCC_WARN_UNUSED_FUNCTION = YES; 233 | GCC_WARN_UNUSED_VARIABLE = YES; 234 | MACOSX_DEPLOYMENT_TARGET = 10.14; 235 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 236 | MTL_FAST_MATH = YES; 237 | ONLY_ACTIVE_ARCH = YES; 238 | SDKROOT = macosx; 239 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 240 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 241 | VERSIONING_SYSTEM = "apple-generic"; 242 | VERSION_INFO_PREFIX = ""; 243 | }; 244 | name = Debug; 245 | }; 246 | 3AA665D623223E220011F310 /* Release */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | ALWAYS_SEARCH_USER_PATHS = NO; 250 | CLANG_ANALYZER_NONNULL = YES; 251 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 252 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 253 | CLANG_CXX_LIBRARY = "libc++"; 254 | CLANG_ENABLE_MODULES = YES; 255 | CLANG_ENABLE_OBJC_ARC = YES; 256 | CLANG_ENABLE_OBJC_WEAK = YES; 257 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 258 | CLANG_WARN_BOOL_CONVERSION = YES; 259 | CLANG_WARN_COMMA = YES; 260 | CLANG_WARN_CONSTANT_CONVERSION = YES; 261 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 262 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 263 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 264 | CLANG_WARN_EMPTY_BODY = YES; 265 | CLANG_WARN_ENUM_CONVERSION = YES; 266 | CLANG_WARN_INFINITE_RECURSION = YES; 267 | CLANG_WARN_INT_CONVERSION = YES; 268 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 269 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 270 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 271 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 272 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 273 | CLANG_WARN_STRICT_PROTOTYPES = YES; 274 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 275 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 276 | CLANG_WARN_UNREACHABLE_CODE = YES; 277 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 278 | CODE_SIGN_IDENTITY = "Mac Developer"; 279 | COPY_PHASE_STRIP = NO; 280 | CURRENT_PROJECT_VERSION = 1; 281 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 282 | ENABLE_NS_ASSERTIONS = NO; 283 | ENABLE_STRICT_OBJC_MSGSEND = YES; 284 | GCC_C_LANGUAGE_STANDARD = gnu11; 285 | GCC_NO_COMMON_BLOCKS = YES; 286 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 287 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 288 | GCC_WARN_UNDECLARED_SELECTOR = YES; 289 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 290 | GCC_WARN_UNUSED_FUNCTION = YES; 291 | GCC_WARN_UNUSED_VARIABLE = YES; 292 | MACOSX_DEPLOYMENT_TARGET = 10.14; 293 | MTL_ENABLE_DEBUG_INFO = NO; 294 | MTL_FAST_MATH = YES; 295 | SDKROOT = macosx; 296 | SWIFT_COMPILATION_MODE = wholemodule; 297 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 298 | VERSIONING_SYSTEM = "apple-generic"; 299 | VERSION_INFO_PREFIX = ""; 300 | }; 301 | name = Release; 302 | }; 303 | 3AA665D823223E220011F310 /* Debug */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | CLANG_ENABLE_MODULES = YES; 307 | CODE_SIGN_IDENTITY = ""; 308 | CODE_SIGN_STYLE = Automatic; 309 | COMBINE_HIDPI_IMAGES = YES; 310 | DEFINES_MODULE = YES; 311 | DEVELOPMENT_TEAM = 5J4GVGN58B; 312 | DYLIB_COMPATIBILITY_VERSION = 1; 313 | DYLIB_CURRENT_VERSION = 1; 314 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 315 | FRAMEWORK_VERSION = A; 316 | INFOPLIST_FILE = PusherMainView/Info.plist; 317 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 318 | LD_RUNPATH_SEARCH_PATHS = ( 319 | "$(inherited)", 320 | "@executable_path/../Frameworks", 321 | "@loader_path/Frameworks", 322 | ); 323 | PRODUCT_BUNDLE_IDENTIFIER = com.rakuten.tech.mobile.PusherMainView; 324 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 325 | SKIP_INSTALL = YES; 326 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 327 | SWIFT_VERSION = 5.0; 328 | }; 329 | name = Debug; 330 | }; 331 | 3AA665D923223E220011F310 /* Release */ = { 332 | isa = XCBuildConfiguration; 333 | buildSettings = { 334 | CLANG_ENABLE_MODULES = YES; 335 | CODE_SIGN_IDENTITY = ""; 336 | CODE_SIGN_STYLE = Automatic; 337 | COMBINE_HIDPI_IMAGES = YES; 338 | DEFINES_MODULE = YES; 339 | DEVELOPMENT_TEAM = 5J4GVGN58B; 340 | DYLIB_COMPATIBILITY_VERSION = 1; 341 | DYLIB_CURRENT_VERSION = 1; 342 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 343 | FRAMEWORK_VERSION = A; 344 | INFOPLIST_FILE = PusherMainView/Info.plist; 345 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 346 | LD_RUNPATH_SEARCH_PATHS = ( 347 | "$(inherited)", 348 | "@executable_path/../Frameworks", 349 | "@loader_path/Frameworks", 350 | ); 351 | PRODUCT_BUNDLE_IDENTIFIER = com.rakuten.tech.mobile.PusherMainView; 352 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 353 | SKIP_INSTALL = YES; 354 | SWIFT_VERSION = 5.0; 355 | }; 356 | name = Release; 357 | }; 358 | /* End XCBuildConfiguration section */ 359 | 360 | /* Begin XCConfigurationList section */ 361 | 3AA665C923223E210011F310 /* Build configuration list for PBXProject "PusherMainView" */ = { 362 | isa = XCConfigurationList; 363 | buildConfigurations = ( 364 | 3AA665D523223E220011F310 /* Debug */, 365 | 3AA665D623223E220011F310 /* Release */, 366 | ); 367 | defaultConfigurationIsVisible = 0; 368 | defaultConfigurationName = Release; 369 | }; 370 | 3AA665D723223E220011F310 /* Build configuration list for PBXNativeTarget "PusherMainView" */ = { 371 | isa = XCConfigurationList; 372 | buildConfigurations = ( 373 | 3AA665D823223E220011F310 /* Debug */, 374 | 3AA665D923223E220011F310 /* Release */, 375 | ); 376 | defaultConfigurationIsVisible = 0; 377 | defaultConfigurationName = Release; 378 | }; 379 | /* End XCConfigurationList section */ 380 | }; 381 | rootObject = 3AA665C623223E210011F310 /* Project object */; 382 | } 383 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/ActionType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APNS 3 | 4 | enum ActionType { 5 | case configure 6 | case devicesList(fromViewController: NSViewController) 7 | case pushTypesList(fromViewController: NSViewController) 8 | case deviceToken(String) 9 | case pushType(String) 10 | case chooseAuthToken(fromViewController: NSViewController) 11 | case alert(message: String, fromWindow: NSWindow?) 12 | case browsingFiles(fromViewController: NSViewController, completion: (_ fileURL: URL) -> Void) 13 | case browsingJSONFiles(fromViewController: NSViewController, completion: (_ fileURL: URL, _ text: String) -> Void) 14 | case selectDevice(device: APNSServiceDevice) 15 | case selectPushType(pushType: String) 16 | case chooseiOSSimulator 17 | case chooseiOSDevice 18 | case chooseAndroidDevice(useLegacyFCM: Bool) 19 | case cancelAuthToken 20 | case saveAuthToken(teamID: String, keyID: String, p8FileURL: URL, p8: String) 21 | case chooseIdentity(fromViewController: NSViewController) 22 | case cancelIdentity 23 | case updateIdentity(identity: SecIdentity) 24 | case dismiss(fromViewController: NSViewController) 25 | case push(_ data: PushData, 26 | completion: (Bool) -> Void) 27 | case enableSaveMenuItem 28 | case saveFile(text: String, fileURL: URL) 29 | case saveFileAs(text: String, fromViewController: NSViewController, completion: (_ fileURL: URL) -> Void) 30 | case payloadDidChange(fileURL: URL?) 31 | } 32 | 33 | extension ActionType: Equatable { 34 | static func == (lhs: ActionType, rhs: ActionType) -> Bool { 35 | switch (lhs, rhs) { 36 | case (.configure, .configure): 37 | return true 38 | 39 | case (let .devicesList(lhsViewController), let .devicesList(rhsViewController)): 40 | return lhsViewController == rhsViewController 41 | 42 | case (let .pushTypesList(lhsViewController), let .pushTypesList(rhsViewController)): 43 | return lhsViewController == rhsViewController 44 | 45 | case (let .deviceToken(lhsString), let .deviceToken(rhsString)), (let .pushType(lhsString), let .pushType(rhsString)): 46 | return lhsString == rhsString 47 | 48 | case (let .chooseAuthToken(lhsViewController), let .chooseAuthToken(rhsViewController)): 49 | return lhsViewController == rhsViewController 50 | 51 | case (let .alert(lhsString, lhsWindow), let .alert(rhsString, rhsWindow)): 52 | return lhsString == rhsString && lhsWindow == rhsWindow 53 | 54 | case (let .browsingFiles(lhsViewController, _), let .browsingFiles(rhsViewController, _)): 55 | return lhsViewController == rhsViewController 56 | 57 | case (let .browsingJSONFiles(lhsViewController, _), let .browsingJSONFiles(rhsViewController, _)): 58 | return lhsViewController == rhsViewController 59 | 60 | case (let .selectDevice(lhsAPNSServiceDevice), let .selectDevice(rhsAPNSServiceDevice)): 61 | return lhsAPNSServiceDevice == rhsAPNSServiceDevice 62 | 63 | case (let .selectPushType(lhsPushType), let .selectPushType(rhsPushType)): 64 | return lhsPushType == rhsPushType 65 | 66 | case (.chooseiOSSimulator, .chooseiOSSimulator): 67 | return true 68 | 69 | case (.chooseiOSDevice, .chooseiOSDevice): 70 | return true 71 | 72 | case (let .chooseAndroidDevice(lhs), let .chooseAndroidDevice(rhs)): 73 | return lhs == rhs 74 | 75 | case (.cancelAuthToken, .cancelAuthToken): 76 | return true 77 | 78 | case (let .saveAuthToken(lhs1, lhs2, lhs3, lhs4), let .saveAuthToken(rhs1, rhs2, rhs3, rhs4)): 79 | return lhs1 == rhs1 && lhs2 == rhs2 && lhs3 == rhs3 && lhs4 == rhs4 80 | 81 | case (let .chooseIdentity(lhsViewController), let .chooseIdentity(rhsViewController)): 82 | return lhsViewController == rhsViewController 83 | 84 | case (.cancelIdentity, .cancelIdentity): 85 | return true 86 | 87 | case (let .updateIdentity(lhsSecIdentity), let .updateIdentity(rhsSecIdentity)): 88 | return lhsSecIdentity == rhsSecIdentity 89 | 90 | case (let .dismiss(lhsViewController), let .dismiss(rhsViewController)): 91 | return lhsViewController == rhsViewController 92 | 93 | case (let .push(lhs, _), 94 | let .push(rhs, _)): 95 | return lhs == rhs 96 | 97 | case (.enableSaveMenuItem, .enableSaveMenuItem): 98 | return true 99 | 100 | case (let .saveFile(lhsString, lhsURL), let .saveFile(rhsString, rhsURL)): 101 | return lhsString == rhsString && lhsURL == rhsURL 102 | 103 | case (let .saveFileAs(lhsString, lhsViewController, _), let .saveFileAs(rhsString, rhsViewController, _)): 104 | return lhsString == rhsString && lhsViewController == rhsViewController 105 | 106 | case (let .payloadDidChange(lhsURL), let .payloadDidChange(rhsURL)): 107 | return lhsURL == rhsURL 108 | 109 | default: 110 | return false 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/AuthTokenViewcontroller.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APNS 3 | 4 | final class AuthTokenViewcontroller: NSViewController { 5 | private var pusherStore: PusherInteracting? 6 | private var p8FileURL: URL? 7 | @IBOutlet private var keyIDTextField: NSTextField! 8 | @IBOutlet private var teamIDTextField: NSTextField! 9 | @IBOutlet private var p8Label: NSTextField! 10 | 11 | required init?(coder: NSCoder) { 12 | super.init(coder: coder) 13 | #if DEBUG 14 | print("\(self.className) init") 15 | #endif 16 | } 17 | 18 | deinit { 19 | #if DEBUG 20 | print("\(self.className) deinit") 21 | #endif 22 | } 23 | 24 | static func create(pusherStore: PusherInteracting) -> AuthTokenViewcontroller? { 25 | let bundle = Bundle(for: PusherViewController.self) 26 | let storyboard = NSStoryboard(name: "Pusher", bundle: bundle) 27 | guard let viewController = storyboard.instantiateController(withIdentifier: "AuthTokenViewcontroller") as? AuthTokenViewcontroller else { 28 | return nil 29 | } 30 | viewController.pusherStore = pusherStore 31 | return viewController 32 | } 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | keyIDTextField.stringValue = pusherStore?.authToken?.keyID ?? "" 37 | teamIDTextField.stringValue = pusherStore?.authToken?.teamID ?? "" 38 | p8Label.stringValue = pusherStore?.authToken?.p8FileURLString.prettyDisplay ?? "" 39 | 40 | if let urlString = pusherStore?.authToken?.p8FileURLString { 41 | p8FileURL = URL(string: urlString) 42 | } 43 | } 44 | 45 | // MARK: - Actions 46 | 47 | @IBAction private func didTapOpenP8Button(_ sender: Any) { 48 | pusherStore?.dispatch(actionType: .browsingFiles(fromViewController: self, completion: { (p8FileURL) in 49 | self.p8FileURL = p8FileURL 50 | self.p8Label.stringValue = p8FileURL.absoluteString.prettyDisplay 51 | })) 52 | } 53 | 54 | @IBAction private func didTapCancelButton(_ sender: Any) { 55 | pusherStore?.dispatch(actionType: .cancelAuthToken) 56 | pusherStore?.dispatch(actionType: .dismiss(fromViewController: self)) 57 | } 58 | 59 | @IBAction private func didTapValidateButton(_ sender: Any) { 60 | guard !teamIDTextField.stringValue.isEmpty else { 61 | pusherStore?.dispatch(actionType: .alert(message: "please.enter.team.id".localized, fromWindow: view.window)) 62 | return 63 | } 64 | 65 | guard !keyIDTextField.stringValue.isEmpty else { 66 | pusherStore?.dispatch(actionType: .alert(message: "please.enter.key.id".localized, fromWindow: view.window)) 67 | return 68 | } 69 | 70 | guard let p8FileURL = p8FileURL, 71 | let p8String = try? String(contentsOf: p8FileURL, encoding: .utf8) else { 72 | pusherStore?.dispatch(actionType: .alert(message: "error.p8.file.is.incorrect".localized, fromWindow: view.window)) 73 | return 74 | } 75 | 76 | pusherStore?.dispatch(actionType: .saveAuthToken(teamID: teamIDTextField.stringValue, 77 | keyID: keyIDTextField.stringValue, 78 | p8FileURL: p8FileURL, 79 | p8: p8String)) 80 | pusherStore?.dispatch(actionType: .dismiss(fromViewController: self)) 81 | } 82 | } 83 | 84 | private extension String { 85 | var prettyDisplay: String { 86 | let headerLength = "file://".count 87 | let totalLength = self.count 88 | return "\(self.suffix(totalLength - headerLength))" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/DefaultPayloads.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum DefaultPayloads { 4 | static let apns = """ 5 | { 6 | "aps":{ 7 | "alert":"Test", 8 | "sound":"default", 9 | "badge":1 10 | } 11 | } 12 | """ 13 | static let fcmV1 = """ 14 | { 15 | "message":{ 16 | "notification":{ 17 | "title":"Firebase Cloud Message Title", 18 | "body":"Firebase Cloud Message Body", 19 | "subtitle":"Firebase Cloud Message Subtitle" 20 | } 21 | } 22 | } 23 | """ 24 | static let fcmLegacy = """ 25 | { 26 | "notification": { 27 | "body":"Firebase Cloud Message Body", 28 | "title":"Firebase Cloud Message Title", 29 | "subtitle":"Firebase Cloud Message Subtitle" 30 | } 31 | } 32 | 33 | """ 34 | } 35 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/DevicesViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APNS 3 | 4 | final class DevicesViewController: NSViewController { 5 | @IBOutlet private var tableView: NSTableView! 6 | private let apnsServiceBrowser = APNSServiceBrowser(serviceType: "pusher") 7 | private var pusherStore: PusherInteracting? 8 | 9 | required init?(coder: NSCoder) { 10 | super.init(coder: coder) 11 | apnsServiceBrowser.delegate = self 12 | #if DEBUG 13 | print("\(self.className) init") 14 | #endif 15 | } 16 | 17 | static func create(pusherStore: PusherInteracting) -> DevicesViewController? { 18 | let bundle = Bundle(for: PusherViewController.self) 19 | let storyboard = NSStoryboard(name: "Pusher", bundle: bundle) 20 | guard let viewController = storyboard.instantiateController(withIdentifier: "DevicesViewController") as? DevicesViewController else { 21 | return nil 22 | } 23 | viewController.pusherStore = pusherStore 24 | return viewController 25 | } 26 | 27 | deinit { 28 | #if DEBUG 29 | print("\(self.className) deinit") 30 | #endif 31 | apnsServiceBrowser.searching = false 32 | } 33 | 34 | // MARK: - Life Cycle 35 | 36 | override func viewDidAppear() { 37 | super.viewDidAppear() 38 | apnsServiceBrowser.searching = true 39 | } 40 | 41 | override func viewDidDisappear() { 42 | super.viewDidDisappear() 43 | apnsServiceBrowser.searching = false 44 | } 45 | 46 | override func viewDidLoad() { 47 | super.viewDidLoad() 48 | tableView.dataSource = self 49 | tableView.delegate = self 50 | } 51 | 52 | // MARK: - Actions 53 | 54 | @IBAction private func didTapCloseButton(_ sender: Any) { 55 | pusherStore?.dispatch(actionType: .dismiss(fromViewController: self)) 56 | } 57 | } 58 | 59 | extension DevicesViewController: NSTableViewDataSource { 60 | func numberOfRows(in tableView: NSTableView) -> Int { 61 | return apnsServiceBrowser.devices.count 62 | } 63 | } 64 | 65 | extension DevicesViewController: NSTableViewDelegate { 66 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 67 | guard let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DeviceTableCellView"), 68 | owner: nil) as? NSTableCellView, 69 | let identifier = tableColumn?.identifier else { 70 | return nil 71 | } 72 | 73 | switch identifier.rawValue { 74 | case "name": 75 | cell.textField?.stringValue = apnsServiceBrowser.devices[row].displayName 76 | case "token": 77 | cell.textField?.stringValue = apnsServiceBrowser.devices[row].token 78 | case "appID": 79 | cell.textField?.stringValue = apnsServiceBrowser.devices[row].appID 80 | case "type": 81 | cell.textField?.stringValue = apnsServiceBrowser.devices[row].type 82 | default: () 83 | } 84 | 85 | return cell 86 | } 87 | 88 | func tableViewSelectionDidChange(_ notification: Notification) { 89 | guard tableView.selectedRow != -1 else { 90 | return 91 | } 92 | let device = apnsServiceBrowser.devices[tableView.selectedRow] 93 | pusherStore?.dispatch(actionType: .selectDevice(device: device)) 94 | pusherStore?.dispatch(actionType: .dismiss(fromViewController: self)) 95 | } 96 | } 97 | 98 | extension DevicesViewController: APNSServiceBrowsing { 99 | func didUpdateDevices() { 100 | tableView.reloadData() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/Dictionary+Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension [String: Any] { 4 | /// Update the APNS Payload with a timestamp. 5 | /// 6 | /// - Parameter timestamp: the timestamp to update. 7 | mutating func update(with timestamp: Int) { 8 | var aps = self["aps"] as? [String: Any] 9 | aps?["timestamp"] = timestamp 10 | self["aps"] = aps 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/ErrorState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ErrorState: Equatable { 4 | var error: NSError 5 | var actionType: ActionType 6 | } 7 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/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 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2019 Rakuten. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/InterfaceFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct InterfaceFactory { 4 | static func create(for storyboardName: String) -> T? { 5 | let aClass = T.self 6 | let bundle = Bundle(for: aClass.self) 7 | let storyboard = NSStoryboard(name: storyboardName, bundle: bundle) 8 | let absoluteIdentifier = NSStringFromClass(aClass) 9 | 10 | guard let identifier = absoluteIdentifier.components(separatedBy: ".").last, 11 | let viewController = storyboard.instantiateController(withIdentifier: identifier) as? T else { 12 | return nil 13 | } 14 | return viewController 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/JSONTextView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AppKit 3 | import Highlight 4 | 5 | final class JSONTextView: NSTextView { 6 | 7 | let syntaxProvider = JsonSyntaxHighlightProvider() 8 | 9 | override var string: String { 10 | get { super.string } 11 | set { 12 | textStorage?.setAttributedString(makeHighlightedString(from: newValue)) 13 | } 14 | } 15 | 16 | override func didChangeText() { 17 | super.didChangeText() 18 | 19 | textStorage?.beginEditing() 20 | let highlightedString = makeHighlightedString(from: string) 21 | highlightedString.enumerateAttributes(in: NSRange(location: 0, length: highlightedString.length)) { attr, range, _ in 22 | textStorage?.setAttributes(attr, range: range) 23 | } 24 | textStorage?.endEditing() 25 | } 26 | 27 | func highlightError(at index: Int) { 28 | textStorage?.beginEditing() 29 | let safeIndex = min(string.count - 1, index) 30 | var attributes = textStorage?.attributes(at: safeIndex, effectiveRange: nil) 31 | attributes?[NSAttributedString.Key.backgroundColor] = NSColor.red 32 | textStorage?.setAttributes(attributes, range: NSRange(location: safeIndex, length: 1)) 33 | textStorage?.endEditing() 34 | } 35 | 36 | private func makeHighlightedString(from string: String) -> NSMutableAttributedString { 37 | let mutableString = NSMutableAttributedString(string: string) 38 | syntaxProvider.highlight(mutableString, as: .json) 39 | return mutableString 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/Keychain.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Keychain { 4 | static func string(for key: String) -> String? { 5 | guard let service = Bundle.main.bundleIdentifier else { 6 | return nil 7 | } 8 | 9 | let queryLoad: [String: Any] = [ 10 | kSecClass as String: kSecClassGenericPassword, 11 | kSecAttrService as String: service, 12 | kSecAttrAccount as String: key, 13 | kSecReturnData as String: true, 14 | kSecMatchLimit as String: kSecMatchLimitOne 15 | ] 16 | 17 | var result: AnyObject? 18 | let resultCodeLoad = SecItemCopyMatching(queryLoad as CFDictionary, &result) 19 | if resultCodeLoad == errSecSuccess, 20 | let resultVal = result as? Data, 21 | let keyValue = String(data: resultVal, encoding: .utf8) { 22 | return keyValue 23 | } 24 | return nil 25 | } 26 | 27 | static func set(value: String?, forKey key: String) { 28 | guard let service = Bundle.main.bundleIdentifier else { 29 | return 30 | } 31 | 32 | let queryFind: [String: Any] = [ 33 | kSecClass as String: kSecClassGenericPassword, 34 | kSecAttrService as String: service, 35 | kSecAttrAccount as String: key 36 | ] 37 | 38 | guard let valueNotNil = value, let data = valueNotNil.data(using: .utf8) else { 39 | SecItemDelete(queryFind as CFDictionary) 40 | return 41 | } 42 | 43 | let updatedAttributes: [String: Any] = [ 44 | kSecValueData as String: data 45 | ] 46 | 47 | var resultCode = SecItemUpdate(queryFind as CFDictionary, updatedAttributes as CFDictionary) 48 | 49 | if resultCode == errSecItemNotFound { 50 | let queryAdd: [String: Any] = [ 51 | kSecClass as String: kSecClassGenericPassword, 52 | kSecAttrService as String: service, 53 | kSecAttrAccount as String: key, 54 | kSecValueData as String: data, 55 | kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly 56 | ] 57 | 58 | resultCode = SecItemAdd(queryAdd as CFDictionary, nil) 59 | } 60 | 61 | if resultCode != errSecSuccess { 62 | NSException(name: NSExceptionName(rawValue: "Keychain Error"), reason: "Unable to store data \(resultCode)", userInfo: nil).raise() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/Localizable.strings: -------------------------------------------------------------------------------- 1 | // Titles 2 | "app.title" = "The macOS Push Tester App"; 3 | "cancel" = "Cancel"; 4 | "close" = "Close"; 5 | "enter.device.token" = "Enter a device token"; 6 | "enter.apns.collapse.id" = "Enter APNs Collapse ID"; 7 | "enter.your.app.bundle.id" = "Enter your app bundle ID"; 8 | "enter.apns.priority" = "Enter APNs priority"; 9 | "enter.apns.push.type" = "Enter APNs push type"; 10 | "select.push.type" = "Select"; 11 | "enter.fcm.project.id" = "Enter Firebase Project ID"; 12 | "enter.fcm.server.key" = "Enter Server Key"; 13 | 14 | // Messages 15 | "please.enter.team.id" = "Please enter Team ID"; 16 | "please.enter.key.id" = "Please enter Key ID"; 17 | "please.enter.an app.bundle.id" = "Please enter an app bundle ID"; 18 | "please.enter.firebase.project.id" = "Please enter a Firebase Project ID"; 19 | "please.select.an.apns.method" = "Please select an APNs method"; 20 | "please.select.an.apns.certificate" = "Please select an APNs Certificate"; 21 | "please.enter.a.device.token" = "Please enter a Device Token"; 22 | "please.enter.a.server.key" = "Please enter a Server Key"; 23 | "choose.identity" = "Choose the identity to use for delivering notifications: \n(Issued by Apple in the Provisioning Portal)"; 24 | 25 | // Error 26 | "error.json.file.is.incorrect" = "JSON file is incorrect"; 27 | "error.p8.file.is.incorrect" = "p8 file is incorrect"; 28 | "error.no.identity" = "No identity set."; 29 | "error.save.file" = "An error occured while saving the file."; 30 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/MainPlayground.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import PlaygroundSupport 3 | import PusherMainView 4 | 5 | let bundle = Bundle(for: PusherViewController.self) 6 | let storyboard = NSStoryboard(name: "Pusher", bundle: bundle) 7 | let mainVewController = storyboard.instantiateController(withIdentifier: "PusherViewController") as? PusherViewController 8 | 9 | PlaygroundPage.current.liveView = mainVewController 10 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/MainPlayground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/NSApplication+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private enum MenuItemName { 4 | static let file = "File" 5 | static let save = "Save…" 6 | } 7 | 8 | extension NSApplication { 9 | var fileMenu: NSMenu? { 10 | return NSApplication.shared.mainMenu?.item(withTitle: MenuItemName.file)?.submenu 11 | } 12 | 13 | var saveMenuItem: NSMenuItem? { 14 | return fileMenu?.item(withTitle: MenuItemName.save) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/PlistFinder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct PlistFinder { 4 | static func model(for plistName: String, and aClass: AnyClass) -> T? { 5 | let bundle = Bundle(for: aClass) 6 | 7 | guard let url = bundle.url(forResource: plistName, withExtension: "plist"), 8 | let data = try? Data(contentsOf: url) else { 9 | return nil 10 | } 11 | 12 | return try? PropertyListDecoder().decode(T.self, from: data) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/PushData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APNS 3 | 4 | enum PushData: Equatable { 5 | case apns(_ data: APNSPushData) 6 | case fcm(_ data: FCMPushData) 7 | 8 | var payload: String { 9 | switch self { 10 | case .apns(let data): 11 | return data.payload 12 | case .fcm(let data): 13 | return data.payload 14 | } 15 | } 16 | } 17 | 18 | struct APNSPushData: Equatable { 19 | let payload: String 20 | let destination: Destination 21 | let deviceToken: String? 22 | let appBundleID: String? 23 | let priority: Int 24 | let collapseID: String? 25 | let sandbox: Bool 26 | let pushType: String 27 | } 28 | 29 | struct FCMPushData: Equatable { 30 | let payload: String 31 | let destination: Destination 32 | let deviceToken: String? 33 | let serverKey: String? 34 | let projectID: String? 35 | let collapseID: String? 36 | let legacyFCM: Bool 37 | } 38 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/PushTesterError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum PushTesterError: Error { 4 | case invalidJson(NSError) 5 | } 6 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/PushTypesViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APNS 3 | 4 | final class PushTypesViewController: NSViewController { 5 | @IBOutlet private var tableView: NSTableView! 6 | var pusherStore: PusherInteracting? 7 | private let pushTypes: [String] 8 | private let cellIdentifier = "PushTypeTableCellView" 9 | 10 | required init?(coder: NSCoder) { 11 | guard let pushTypes = PlistFinder<[String]>.model(for: "pushtypes", and: PushTypesViewController.self) else { 12 | return nil 13 | } 14 | 15 | self.pushTypes = pushTypes 16 | 17 | super.init(coder: coder) 18 | #if DEBUG 19 | print("\(self.className) init") 20 | #endif 21 | } 22 | 23 | deinit { 24 | #if DEBUG 25 | print("\(self.className) deinit") 26 | #endif 27 | } 28 | 29 | // MARK: - Life Cycle 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | tableView.dataSource = self 34 | tableView.delegate = self 35 | } 36 | 37 | // MARK: - Actions 38 | 39 | @IBAction private func didTapCloseButton(_ sender: Any) { 40 | pusherStore?.dispatch(actionType: .dismiss(fromViewController: self)) 41 | } 42 | } 43 | 44 | extension PushTypesViewController: NSTableViewDataSource { 45 | func numberOfRows(in tableView: NSTableView) -> Int { 46 | pushTypes.count 47 | } 48 | } 49 | 50 | extension PushTypesViewController: NSTableViewDelegate { 51 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 52 | guard let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), 53 | owner: nil) as? NSTableCellView, 54 | let identifier = tableColumn?.identifier else { 55 | return nil 56 | } 57 | 58 | if identifier.rawValue == "name" { 59 | cell.textField?.stringValue = pushTypes[row] 60 | } 61 | 62 | return cell 63 | } 64 | 65 | func tableViewSelectionDidChange(_ notification: Notification) { 66 | guard tableView.selectedRow != NSNotFound else { 67 | return 68 | } 69 | let pushType = pushTypes[tableView.selectedRow] 70 | pusherStore?.dispatch(actionType: .selectPushType(pushType: pushType)) 71 | pusherStore?.dispatch(actionType: .dismiss(fromViewController: self)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/PusherMainView.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for PusherMainView. 4 | FOUNDATION_EXPORT double PusherMainViewVersionNumber; 5 | 6 | //! Project version string for PusherMainView. 7 | FOUNDATION_EXPORT const unsigned char PusherMainViewVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/PusherReducer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct PusherReducer { 4 | func reduce(actionType: ActionType, state: PusherState) -> PusherState { 5 | var newState = state 6 | 7 | switch actionType { 8 | case .selectDevice(let device): 9 | newState.deviceTokenString = device.token 10 | newState.appOrProjectID = device.appID 11 | 12 | case .selectPushType(let pushType), .pushType(let pushType): 13 | newState.pushType = pushType 14 | 15 | case .deviceToken(let deviceToken): 16 | newState.deviceTokenString = deviceToken 17 | 18 | case .chooseAuthToken: 19 | newState.certificateRadioState = .off 20 | newState.authTokenRadioState = .on 21 | 22 | case .cancelAuthToken: 23 | newState.authTokenRadioState = .off 24 | 25 | case .saveAuthToken: 26 | newState.authTokenRadioState = .on 27 | 28 | case .chooseIdentity: 29 | newState.certificateRadioState = .on 30 | newState.authTokenRadioState = .off 31 | 32 | case .cancelIdentity: 33 | newState.certificateRadioState = .off 34 | 35 | case .updateIdentity: 36 | newState.certificateRadioState = .on 37 | 38 | case .chooseiOSDevice: 39 | newState.iOSSimulatorRadioState = .off 40 | newState.iOSDeviceRadioState = .on 41 | newState.androidDeviceRadioState = .off 42 | 43 | case .chooseiOSSimulator: 44 | newState.iOSSimulatorRadioState = .on 45 | newState.iOSDeviceRadioState = .off 46 | newState.androidDeviceRadioState = .off 47 | 48 | case .chooseAndroidDevice(let legacyFCM): 49 | newState.iOSSimulatorRadioState = .off 50 | newState.iOSDeviceRadioState = .off 51 | newState.androidDeviceRadioState = .on 52 | newState.legacyFCMCheckboxState = legacyFCM ? .on : .off 53 | 54 | case .configure, .enableSaveMenuItem: 55 | newState.appTitle = "app.title".localized 56 | 57 | case .saveFile(_, let fileURL): 58 | newState.appTitle = "app.title".localized 59 | newState.fileURL = fileURL 60 | 61 | case .payloadDidChange(let fileURL): 62 | guard fileURL != nil else { 63 | return newState 64 | } 65 | newState.appTitle = "app.title".localized + " *" 66 | 67 | default: () 68 | } 69 | 70 | return newState 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/PusherState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct PusherState: Equatable { 4 | var deviceTokenString: String 5 | var pushType: String 6 | var serverKeyString: String 7 | var appOrProjectID: String 8 | var certificateRadioState: NSControl.StateValue 9 | var authTokenRadioState: NSControl.StateValue 10 | var iOSDeviceRadioState: NSControl.StateValue 11 | var iOSSimulatorRadioState: NSControl.StateValue 12 | var androidDeviceRadioState: NSControl.StateValue 13 | var legacyFCMCheckboxState: NSControl.StateValue 14 | var appTitle: String 15 | var fileURL: URL? 16 | } 17 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/PusherStore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APNS 3 | import FCM 4 | import SecurityInterface.SFChooseIdentityPanel 5 | 6 | enum Destination { 7 | case iOSDevice 8 | case androidDevice 9 | case iOSSimulator 10 | } 11 | 12 | struct AuthToken: Codable { 13 | public let keyID: String 14 | public let teamID: String 15 | public let p8FileURLString: String 16 | } 17 | 18 | protocol PusherInteractable where Self: NSObject { 19 | func newState(state: PusherState) 20 | func newErrorState(_ errorState: ErrorState) 21 | } 22 | 23 | protocol PusherInteracting { 24 | var authToken: AuthToken? { get } 25 | func subscribe(_ pusherInteractable: PusherInteractable) 26 | func unsubscribe(_ pusherInteractable: PusherInteractable) 27 | func dispatch(actionType: ActionType) 28 | } 29 | 30 | final class PusherStore { 31 | private var apnsPusher: APNSPushable 32 | private var fcmPusher: FCMPushable 33 | private let router: Routing 34 | private let reducer = PusherReducer() 35 | public private(set) var authToken: AuthToken? 36 | private var subscribers: [PusherInteractable] = [] 37 | private var state: PusherState = PusherState(deviceTokenString: "", 38 | pushType: "", 39 | serverKeyString: "", 40 | appOrProjectID: "", 41 | certificateRadioState: .off, 42 | authTokenRadioState: .off, 43 | iOSDeviceRadioState: .on, 44 | iOSSimulatorRadioState: .off, 45 | androidDeviceRadioState: .off, 46 | legacyFCMCheckboxState: .on, 47 | appTitle: "") 48 | 49 | init(apnsPusher: APNSPushable, fcmPusher: FCMPushable, router: Routing) { 50 | self.apnsPusher = apnsPusher 51 | self.fcmPusher = fcmPusher 52 | self.router = router 53 | updateAuthToken() 54 | #if DEBUG 55 | print("\(PusherStore.self) init") 56 | #endif 57 | } 58 | 59 | deinit { 60 | #if DEBUG 61 | print("\(PusherStore.self) deinit") 62 | #endif 63 | } 64 | 65 | private func updateAuthToken() { 66 | guard let keyID = Keychain.string(for: "keyID"), 67 | let teamID = Keychain.string(for: "teamID"), 68 | let p8FileURLString = Keychain.string(for: "p8FileURLString") else { 69 | return 70 | } 71 | authToken = AuthToken(keyID: keyID, teamID: teamID, p8FileURLString: p8FileURLString) 72 | } 73 | 74 | private func push(data pushData: PushData, 75 | completion: @escaping (Bool) -> Void) { 76 | switch pushData { 77 | case .apns(let data): 78 | switch data.destination { 79 | 80 | case .iOSSimulator: 81 | guard let appBundleID = data.appBundleID, !appBundleID.isEmpty else { 82 | router.show(message: "please.enter.an app.bundle.id".localized, window: NSApplication.shared.windows.first) 83 | completion(false) 84 | return 85 | } 86 | 87 | apnsPusher.pushToSimulator(payload: data.payload, appBundleID: appBundleID) { [weak self] result in 88 | self?.handlePushResult(result, calling: completion) 89 | } 90 | 91 | case .iOSDevice: 92 | guard let appBundleID = data.appBundleID, !appBundleID.isEmpty else { 93 | router.show(message: "please.enter.an app.bundle.id".localized, window: NSApplication.shared.windows.first) 94 | completion(false) 95 | return 96 | } 97 | 98 | if case .none = apnsPusher.type { 99 | router.show(message: "please.select.an.apns.method".localized, window: NSApplication.shared.windows.first) 100 | completion(false) 101 | return 102 | } 103 | 104 | if case .certificate = apnsPusher.type, apnsPusher.identity == nil { 105 | router.show(message: "please.select.an.apns.certificate".localized, window: NSApplication.shared.windows.first) 106 | completion(false) 107 | return 108 | } 109 | 110 | guard let deviceToken = data.deviceToken, !deviceToken.isEmpty else { 111 | router.show(message: "please.enter.a.device.token".localized, window: NSApplication.shared.windows.first) 112 | completion(false) 113 | return 114 | } 115 | 116 | guard let payloadData = data.payload.data(using: .utf8), 117 | var payload = try? JSONSerialization.jsonObject(with: payloadData, options: .allowFragments) as? [String: Any] else { 118 | completion(false) 119 | return 120 | } 121 | 122 | // Update the timestamp for Live Activity notification payload 123 | // Note: if the timestamp is not the current time, then the push won't be received on the device 124 | if data.pushType == APNsPushType.liveActivity { 125 | payload.update(with: Timestamp.current) 126 | } 127 | 128 | apnsPusher.pushToDevice(deviceToken, 129 | payload: payload, 130 | withTopic: appBundleID, 131 | priority: data.priority, 132 | collapseID: data.collapseID, 133 | inSandbox: data.sandbox, 134 | pushType: data.pushType, 135 | completion: { [weak self] result in 136 | self?.handlePushResult(result, calling: completion) 137 | }) 138 | 139 | case .androidDevice: () 140 | } 141 | case .fcm(let data): 142 | switch data.destination { 143 | 144 | case .androidDevice: 145 | guard let deviceToken = data.deviceToken, !deviceToken.isEmpty else { 146 | router.show(message: "please.enter.a.device.token".localized, window: NSApplication.shared.windows.first) 147 | completion(false) 148 | return 149 | } 150 | 151 | guard let serverKey = data.serverKey, !serverKey.isEmpty else { 152 | router.show(message: "please.enter.a.server.key".localized, window: NSApplication.shared.windows.first) 153 | completion(false) 154 | return 155 | } 156 | 157 | guard let payloadData = data.payload.data(using: .utf8), 158 | let payload = try? JSONSerialization.jsonObject(with: payloadData, options: .allowFragments) as? [String: Any] else { 159 | completion(false) 160 | return 161 | } 162 | 163 | if data.legacyFCM { 164 | fcmPusher.pushUsingLegacyEndpoint(deviceToken, 165 | payload: payload, 166 | collapseID: data.collapseID, 167 | serverKey: serverKey, 168 | completion: { [weak self] result in 169 | self?.handlePushResult(result, calling: completion) 170 | }) 171 | } else { 172 | guard let projectID = data.projectID, !projectID.isEmpty else { 173 | router.show(message: "please.enter.firebase.project.id".localized, window: NSApplication.shared.windows.first) 174 | completion(false) 175 | return 176 | } 177 | 178 | fcmPusher.pushUsingV1Endpoint(deviceToken, 179 | payload: payload, 180 | collapseID: data.collapseID, 181 | serverKey: serverKey, 182 | projectID: projectID, 183 | completion: { [weak self] result in 184 | self?.handlePushResult(result, calling: completion) 185 | }) 186 | } 187 | 188 | case .iOSSimulator, .iOSDevice: () 189 | } 190 | } 191 | } 192 | 193 | private func handlePushResult(_ result: Result, calling completion: (Bool) -> Void) { 194 | switch result { 195 | case .failure(let error): 196 | self.router.show(message: error.localizedDescription, window: NSApplication.shared.windows.first) 197 | completion(false) 198 | 199 | case .success: 200 | completion(true) 201 | } 202 | } 203 | } 204 | 205 | extension PusherStore: PusherInteracting { 206 | func subscribe(_ pusherInteractable: PusherInteractable) { 207 | subscribers.append(pusherInteractable) 208 | pusherInteractable.newState(state: state) // send current state 209 | } 210 | 211 | func unsubscribe(_ pusherInteractable: PusherInteractable) { 212 | guard let index = subscribers.firstIndex(where: { pusherInteractable == $0 }) else { 213 | return 214 | } 215 | subscribers.remove(at: index) 216 | } 217 | 218 | func dispatch(actionType: ActionType) { 219 | switch actionType { 220 | case .devicesList(let fromViewController): 221 | router.presentDevicesList(from: fromViewController, pusherStore: self) 222 | reduce(result: .success(actionType)) 223 | 224 | case .pushTypesList(let fromViewController): 225 | router.presentPushTypesList(from: fromViewController, pusherStore: self) 226 | reduce(result: .success(actionType)) 227 | 228 | case .deviceToken, .pushType: 229 | state = reducer.reduce(actionType: actionType, state: state) 230 | reduce(result: .success(actionType)) 231 | 232 | case .alert(let message, let window): 233 | router.show(message: message, window: window) 234 | reduce(result: .success(actionType)) 235 | 236 | case .browsingFiles(let fromViewController, let completion): 237 | router.browseFiles(from: fromViewController, completion: completion) 238 | reduce(result: .success(actionType)) 239 | 240 | case .browsingJSONFiles(let fromViewController, let completion): 241 | router.browseFiles(from: fromViewController) { fileURL in 242 | guard let jsonString = try? String(contentsOf: fileURL, encoding: .utf8) else { 243 | self.dispatch(actionType: .alert(message: "error.json.file.is.incorrect".localized, fromWindow: NSApplication.shared.mainWindow)) 244 | return 245 | } 246 | completion(fileURL, jsonString) 247 | } 248 | reduce(result: .success(actionType)) 249 | 250 | case .chooseAuthToken(let fromViewController): 251 | updateAuthToken() 252 | router.presentAuthTokenAlert(from: fromViewController, pusherStore: self) 253 | reduce(result: .success(actionType)) 254 | 255 | case .saveAuthToken(let teamID, let keyID, let p8FileURL, let p8): 256 | apnsPusher.type = .token(keyID: keyID, teamID: teamID, p8: p8) 257 | Keychain.set(value: keyID, forKey: "keyID") 258 | Keychain.set(value: teamID, forKey: "teamID") 259 | Keychain.set(value: p8FileURL.absoluteString, forKey: "p8FileURLString") 260 | reduce(result: .success(actionType)) 261 | 262 | case .dismiss(let fromViewController): 263 | router.dismiss(from: fromViewController) 264 | reduce(result: .success(actionType)) 265 | 266 | case .updateIdentity(let identity): 267 | apnsPusher.type = .certificate(identity: identity) 268 | reduce(result: .success(actionType)) 269 | 270 | case .push(let data, 271 | let completion): 272 | do { 273 | try data.payload.validateJSON() 274 | } catch { 275 | router.show(message: "\("error.json.file.is.incorrect".localized)\n\(error.localizedDescription)", 276 | window: NSApplication.shared.windows.first) 277 | reduce(result: .failure(PushTesterError.invalidJson(error as NSError), actionType)) 278 | return 279 | } 280 | push(data: data, 281 | completion: completion) 282 | reduce(result: .success(actionType)) 283 | 284 | case .selectDevice, .cancelAuthToken: 285 | reduce(result: .success(actionType)) 286 | 287 | case .selectPushType: 288 | reduce(result: .success(actionType)) 289 | 290 | case .chooseIdentity(let fromViewController): 291 | let identities = APNSIdentity.identities() 292 | guard !identities.isEmpty else { 293 | state = reducer.reduce(actionType: .cancelIdentity, state: state) 294 | subscribers.forEach { $0.newState(state: state) } 295 | router.show(message: "error.no.identity".localized, window: NSApplication.shared.windows.first) 296 | return 297 | } 298 | let panel = SFChooseIdentityPanel.shared() 299 | panel?.setAlternateButtonTitle("cancel".localized) 300 | panel?.beginSheet(for: fromViewController.view.window, 301 | modalDelegate: self, 302 | didEnd: #selector(chooseIdentityPanelDidEnd(_:returnCode:contextInfo:)), 303 | contextInfo: nil, 304 | identities: identities, 305 | message: "choose.identity".localized) 306 | reduce(result: .success(actionType)) 307 | 308 | case .enableSaveMenuItem: 309 | NSApplication.shared.saveMenuItem?.isEnabled = true 310 | reduce(result: .success(actionType)) 311 | 312 | case .saveFile(let text, let fileURL): 313 | do { 314 | try text.write(to: fileURL, atomically: true, encoding: .utf8) 315 | reduce(result: .success(actionType)) 316 | 317 | } catch { 318 | router.show(message: "error.save.file".localized, window: NSApplication.shared.windows.first) 319 | reduce(result: .failure(error, actionType)) 320 | } 321 | 322 | case .saveFileAs(let text, let fromViewController, let completion): 323 | router.saveFileAs(from: fromViewController) { fileURL in 324 | self.dispatch(actionType: .enableSaveMenuItem) 325 | self.dispatch(actionType: .saveFile(text: text, fileURL: fileURL)) 326 | completion(fileURL) 327 | } 328 | 329 | default: 330 | reduce(result: .success(actionType)) 331 | } 332 | } 333 | 334 | enum StoreResult { 335 | case success(ActionType) 336 | case failure(Error, ActionType) 337 | } 338 | 339 | private func reduce(result: StoreResult) { 340 | switch result { 341 | case .success(let actionType): 342 | let oldState = state 343 | state = reducer.reduce(actionType: actionType, state: state) 344 | if state != oldState { 345 | subscribers.forEach { $0.newState(state: state) } 346 | } 347 | 348 | case .failure(let error, let actionType): 349 | subscribers.forEach { $0.newErrorState(ErrorState(error: error as NSError, actionType: actionType)) } 350 | } 351 | } 352 | 353 | @objc private func chooseIdentityPanelDidEnd(_ sheet: NSWindow, returnCode: Int, contextInfo: Any) { 354 | guard returnCode == NSApplication.ModalResponse.OK.rawValue, let identity = SFChooseIdentityPanel.shared()?.identity() else { 355 | dispatch(actionType: .cancelIdentity) 356 | return 357 | } 358 | 359 | dispatch(actionType: .updateIdentity(identity: identity.takeUnretainedValue() as SecIdentity)) 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/PusherViewController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import APNS 3 | import FCM 4 | 5 | public final class PusherViewController: NSViewController { 6 | @IBOutlet private var deviceTokenTextField: NSTextField! 7 | @IBOutlet private var collapseIdTextField: NSTextField! 8 | @IBOutlet private var payloadTextView: JSONTextView! 9 | @IBOutlet private var appOrProjectIDTextField: NSTextField! 10 | @IBOutlet private var priorityTextField: NSTextField! 11 | @IBOutlet private var sandBoxCheckBox: NSButton! 12 | @IBOutlet private var pushTypeTextField: NSTextField! 13 | @IBOutlet private var selectPushTypeButton: NSButton! 14 | @IBOutlet private var apnsCertificateRadioButton: NSButton! 15 | @IBOutlet private var apnsAuthTokenRadioButton: NSButton! 16 | @IBOutlet private var loadJSONFileButton: NSButton! 17 | @IBOutlet private var sendToiOSDeviceButton: NSButton! 18 | @IBOutlet private var sendToiOSSimulatorButton: NSButton! 19 | @IBOutlet private var sendToAndroidDeviceButton: NSButton! 20 | @IBOutlet private var serverKeyTextField: NSTextField! 21 | @IBOutlet private var legacyFCMCheckbox: NSButton! 22 | @IBOutlet private var deviceSettingsControls: DeviceSettingsControls! 23 | private let pusherStore: PusherInteracting 24 | private var selectedDestination = Destination.iOSDevice 25 | private var jsonFileURL: URL? 26 | 27 | // MARK: - Init 28 | 29 | required init?(coder: NSCoder) { 30 | pusherStore = PusherStore(apnsPusher: APNSPusher(), fcmPusher: FCMPusher(), router: Router()) 31 | super.init(coder: coder) 32 | #if DEBUG 33 | print("\(self.className) init") 34 | #endif 35 | } 36 | 37 | deinit { 38 | pusherStore.unsubscribe(self) 39 | #if DEBUG 40 | print("\(self.className) deinit") 41 | #endif 42 | } 43 | 44 | public static func create() -> PusherViewController? { 45 | let bundle = Bundle(for: PusherViewController.self) 46 | let storyboard = NSStoryboard(name: "Pusher", bundle: bundle) 47 | guard let pusherMainViewController = storyboard.instantiateInitialController() as? PusherViewController else { 48 | return nil 49 | } 50 | return pusherMainViewController 51 | } 52 | 53 | // MARK: - Life Cycle 54 | 55 | public override func viewDidLoad() { 56 | super.viewDidLoad() 57 | 58 | deviceTokenTextField.placeholderString = "enter.device.token".localized 59 | deviceTokenTextField.delegate = self 60 | 61 | pushTypeTextField.placeholderString = "enter.apns.push.type".localized 62 | pushTypeTextField.delegate = self 63 | 64 | collapseIdTextField.placeholderString = "enter.apns.collapse.id".localized 65 | appOrProjectIDTextField.placeholderString = "enter.your.app.bundle.id".localized 66 | priorityTextField.placeholderString = "enter.apns.priority".localized 67 | selectPushTypeButton.title = "select.push.type".localized 68 | serverKeyTextField.placeholderString = "enter.fcm.server.key".localized 69 | 70 | priorityTextField.stringValue = "10" 71 | 72 | payloadTextView.isRichText = false 73 | payloadTextView.isAutomaticTextCompletionEnabled = false 74 | payloadTextView.isAutomaticQuoteSubstitutionEnabled = false 75 | payloadTextView.string = DefaultPayloads.apns 76 | payloadTextView.delegate = self 77 | 78 | pusherStore.subscribe(self) 79 | } 80 | 81 | public override func viewDidAppear() { 82 | super.viewDidAppear() 83 | pusherStore.dispatch(actionType: .configure) 84 | } 85 | 86 | private func updateDefaultPayload(state: PusherState) { 87 | guard payloadTextView.string.isDefaultPayload else { 88 | return 89 | } 90 | 91 | if state.androidDeviceRadioState == .on { 92 | if state.legacyFCMCheckboxState == .on { 93 | payloadTextView.string = DefaultPayloads.fcmLegacy 94 | } else { 95 | payloadTextView.string = DefaultPayloads.fcmV1 96 | } 97 | } else { 98 | payloadTextView.string = DefaultPayloads.apns 99 | } 100 | } 101 | 102 | // MARK: - Actions 103 | 104 | @IBAction func saveFile(_ sender: Any) { 105 | guard let jsonFileURL = jsonFileURL else { 106 | return 107 | } 108 | pusherStore.dispatch(actionType: .saveFile(text: payloadTextView.string, fileURL: jsonFileURL)) 109 | } 110 | 111 | @IBAction func saveFileAs(_ sender: Any) { 112 | pusherStore.dispatch(actionType: .saveFileAs(text: payloadTextView.string, fromViewController: self, completion: { fileURL in 113 | self.jsonFileURL = fileURL 114 | })) 115 | } 116 | 117 | @IBAction func chooseIdentity(_ sender: Any) { 118 | pusherStore.dispatch(actionType: .chooseIdentity(fromViewController: self)) 119 | } 120 | 121 | @IBAction func chooseAuthenticationToken(_ sender: Any) { 122 | pusherStore.dispatch(actionType: .chooseAuthToken(fromViewController: self)) 123 | } 124 | 125 | @IBAction func chooseDestination(_ sender: Any) { 126 | guard let button = sender as? NSButton else { 127 | return 128 | } 129 | switch button { 130 | case sendToiOSDeviceButton: 131 | selectedDestination = .iOSDevice 132 | pusherStore.dispatch(actionType: .chooseiOSDevice) 133 | case sendToiOSSimulatorButton: 134 | selectedDestination = .iOSSimulator 135 | pusherStore.dispatch(actionType: .chooseiOSSimulator) 136 | case sendToAndroidDeviceButton: 137 | selectedDestination = .androidDevice 138 | pusherStore.dispatch(actionType: .chooseAndroidDevice(useLegacyFCM: legacyFCMCheckbox.state == .on)) 139 | default: () 140 | } 141 | } 142 | 143 | @IBAction func loadJSONFile(_ sender: Any) { 144 | pusherStore.dispatch(actionType: .browsingJSONFiles(fromViewController: self, completion: { jsonFileURL, text in 145 | self.jsonFileURL = jsonFileURL 146 | self.payloadTextView.string = text 147 | self.pusherStore.dispatch(actionType: .enableSaveMenuItem) 148 | })) 149 | } 150 | 151 | @IBAction func sendPush(_ sender: Any) { 152 | let isAndroidSelected = sendToAndroidDeviceButton.state == .on 153 | if isAndroidSelected { 154 | pusherStore.dispatch(actionType: .push(.fcm(FCMPushData( 155 | payload: payloadTextView.string, 156 | destination: selectedDestination, 157 | deviceToken: deviceTokenTextField.stringValue, 158 | serverKey: serverKeyTextField.stringValue, 159 | projectID: appOrProjectIDTextField.stringValue, 160 | collapseID: collapseIdTextField.stringValue, 161 | legacyFCM: legacyFCMCheckbox.state == .on))) { _ in }) 162 | } else { 163 | pusherStore.dispatch(actionType: .push(.apns(APNSPushData( 164 | payload: payloadTextView.string, 165 | destination: selectedDestination, 166 | deviceToken: deviceTokenTextField.stringValue, 167 | appBundleID: appOrProjectIDTextField.stringValue, 168 | priority: priorityTextField?.integerValue ?? 10, 169 | collapseID: collapseIdTextField.stringValue, 170 | sandbox: sandBoxCheckBox.state.rawValue == 1, 171 | pushType: pushTypeTextField.stringValue))) { _ in }) 172 | } 173 | } 174 | 175 | @IBAction func selectDevice(_ sender: Any) { 176 | pusherStore.dispatch(actionType: .devicesList(fromViewController: self)) 177 | } 178 | 179 | @IBAction func setLegacyFCM(_ sender: Any) { 180 | pusherStore.dispatch(actionType: .chooseAndroidDevice(useLegacyFCM: legacyFCMCheckbox.state == .on)) 181 | } 182 | 183 | @IBAction func selectPushType(_ sender: Any) { 184 | pusherStore.dispatch(actionType: .pushTypesList(fromViewController: self)) 185 | } 186 | } 187 | 188 | extension PusherViewController: PusherInteractable { 189 | func newState(state: PusherState) { 190 | deviceTokenTextField.stringValue = state.deviceTokenString 191 | pushTypeTextField.stringValue = state.pushType 192 | appOrProjectIDTextField.stringValue = state.appOrProjectID 193 | serverKeyTextField.stringValue = state.serverKeyString 194 | apnsCertificateRadioButton.state = state.certificateRadioState 195 | apnsAuthTokenRadioButton.state = state.authTokenRadioState 196 | sendToiOSDeviceButton.state = state.iOSDeviceRadioState 197 | sendToiOSSimulatorButton.state = state.iOSSimulatorRadioState 198 | sendToAndroidDeviceButton.state = state.androidDeviceRadioState 199 | legacyFCMCheckbox.state = state.legacyFCMCheckboxState 200 | appOrProjectIDTextField.isHidden = state.androidDeviceRadioState == .on && state.legacyFCMCheckboxState == .on 201 | deviceSettingsControls.setVisible(for: selectedDestination) 202 | view.window?.title = state.appTitle 203 | 204 | if state.androidDeviceRadioState == .on { 205 | appOrProjectIDTextField.placeholderString = "enter.fcm.project.id".localized 206 | } else { 207 | appOrProjectIDTextField.placeholderString = "enter.your.app.bundle.id".localized 208 | } 209 | 210 | updateDefaultPayload(state: state) 211 | } 212 | 213 | func newErrorState(_ errorState: ErrorState) { 214 | #if DEBUG 215 | NSLog("\(errorState.error) for \(errorState.actionType)") 216 | #endif 217 | 218 | guard let pushTesterError = errorState.error as? PushTesterError, 219 | case let PushTesterError.invalidJson(error) = pushTesterError, 220 | let errorIndex = error.userInfo["NSJSONSerializationErrorIndex"] as? Int else { 221 | return 222 | } 223 | 224 | payloadTextView.highlightError(at: errorIndex) 225 | } 226 | } 227 | 228 | // MARK: - NSTextFieldDelegate 229 | 230 | extension PusherViewController: NSTextFieldDelegate { 231 | public func controlTextDidChange(_ notification: Notification) { 232 | let textfield = notification.object as? NSTextField 233 | 234 | switch textfield { 235 | case deviceTokenTextField: 236 | let deviceToken = deviceTokenTextField.stringValue 237 | pusherStore.dispatch(actionType: .deviceToken(deviceToken)) 238 | 239 | case pushTypeTextField: 240 | let pushType = pushTypeTextField.stringValue 241 | pusherStore.dispatch(actionType: .pushType(pushType)) 242 | 243 | default: () 244 | } 245 | } 246 | } 247 | 248 | // MARK: - NSTextViewDelegate 249 | 250 | extension PusherViewController: NSTextViewDelegate { 251 | public func textDidChange(_ notification: Notification) { 252 | pusherStore.dispatch(actionType: .payloadDidChange(fileURL: jsonFileURL)) 253 | } 254 | } 255 | 256 | // MARK: - DeviceSettingsControls 257 | 258 | @objc final class DeviceSettingsControls: NSObject { 259 | @IBOutlet private weak var deviceTokenTextField: NSTextField! 260 | @IBOutlet private weak var orLabel: NSTextField! 261 | @IBOutlet private weak var selectDeviceButtonContainer: NSView! 262 | @IBOutlet private weak var apnsButtonsContainer: NSView! 263 | @IBOutlet private weak var priorityTextField: NSTextField! 264 | @IBOutlet private weak var collapseIdTextField: NSTextField! 265 | @IBOutlet private weak var sandBoxCheckBox: NSButton! 266 | @IBOutlet private weak var pushTypeTextField: NSTextField! 267 | @IBOutlet private weak var selectPushTypeButton: NSButton! 268 | @IBOutlet private weak var serverKeyTextFieldContainter: NSView! 269 | 270 | private var iOSControls: [NSView] { 271 | [deviceTokenTextField, orLabel, selectDeviceButtonContainer, apnsButtonsContainer, 272 | priorityTextField, collapseIdTextField, sandBoxCheckBox, pushTypeTextField, selectPushTypeButton] 273 | } 274 | private var androidControls: [NSView] { 275 | [deviceTokenTextField, serverKeyTextFieldContainter, collapseIdTextField] 276 | } 277 | 278 | func setVisible(for destination: Destination) { 279 | (iOSControls + androidControls).forEach { $0.isHidden = true } 280 | switch destination { 281 | case .iOSDevice: 282 | iOSControls.forEach { $0.isHidden = false } 283 | case .androidDevice: 284 | androidControls.forEach { $0.isHidden = false } 285 | case .iOSSimulator: () 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/Router.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol Routing { 4 | func presentDevicesList(from fromViewController: NSViewController, pusherStore: PusherInteracting) 5 | func presentPushTypesList(from fromViewController: NSViewController, pusherStore: PusherInteracting) 6 | func presentAuthTokenAlert(from fromViewController: NSViewController, pusherStore: PusherInteracting) 7 | func show(message: String, window: NSWindow?) 8 | func browseFiles(from fromViewController: NSViewController, completion: @escaping (_ p8FileURL: URL) -> Void) 9 | func saveFileAs(from fromViewController: NSViewController, completion: @escaping (_ fileURL: URL) -> Void) 10 | func dismiss(from fromViewController: NSViewController) 11 | } 12 | 13 | struct Router { 14 | private func presentAsSheet(_ viewController: NSViewController, 15 | from fromViewController: NSViewController) { 16 | fromViewController.presentAsSheet(viewController) 17 | viewController.view.window?.minSize = fromViewController.view.window?.minSize ?? .zero 18 | viewController.view.window?.maxSize = fromViewController.view.window?.minSize ?? .zero 19 | } 20 | } 21 | 22 | extension Router: Routing { 23 | func presentDevicesList(from fromViewController: NSViewController, pusherStore: PusherInteracting) { 24 | guard let viewController = DevicesViewController.create(pusherStore: pusherStore) else { 25 | return 26 | } 27 | presentAsSheet(viewController, from: fromViewController) 28 | } 29 | 30 | func presentPushTypesList(from fromViewController: NSViewController, pusherStore: PusherInteracting) { 31 | guard let viewController = InterfaceFactory.create(for: "Pusher") else { 32 | return 33 | } 34 | viewController.pusherStore = pusherStore 35 | presentAsSheet(viewController, from: fromViewController) 36 | } 37 | 38 | func presentAuthTokenAlert(from fromViewController: NSViewController, pusherStore: PusherInteracting) { 39 | guard let viewController = AuthTokenViewcontroller.create(pusherStore: pusherStore) else { 40 | return 41 | } 42 | presentAsSheet(viewController, from: fromViewController) 43 | } 44 | 45 | func show(message: String, window: NSWindow?) { 46 | NSAlert.show(message: message, window: window) 47 | } 48 | 49 | func browseFiles(from fromViewController: NSViewController, completion: @escaping (_ p8FileURL: URL) -> Void) { 50 | guard let window = fromViewController.view.window else { return } 51 | 52 | let panel = NSOpenPanel() 53 | panel.canChooseFiles = true 54 | panel.canChooseDirectories = false 55 | panel.allowsMultipleSelection = false 56 | 57 | panel.beginSheetModal(for: window) { (result) in 58 | if result.rawValue == NSApplication.ModalResponse.OK.rawValue, 59 | let p8fileURL = panel.urls.first { 60 | completion(p8fileURL) 61 | } 62 | } 63 | } 64 | 65 | func saveFileAs(from fromViewController: NSViewController, completion: @escaping (_ fileURL: URL) -> Void) { 66 | guard let window = fromViewController.view.window else { return } 67 | 68 | let panel = NSSavePanel() 69 | 70 | panel.beginSheetModal(for: window) { result in 71 | if result.rawValue == NSApplication.ModalResponse.OK.rawValue, 72 | let fileURL = panel.url { 73 | completion(fileURL) 74 | } 75 | } 76 | } 77 | 78 | func dismiss(from fromViewController: NSViewController) { 79 | fromViewController.presentingViewController?.dismiss(fromViewController) 80 | } 81 | } 82 | 83 | private extension NSAlert { 84 | static func show(message: String, window: NSWindow?) { 85 | guard let window = window else { 86 | return 87 | } 88 | let alert = NSAlert() 89 | alert.messageText = message 90 | alert.addButton(withTitle: "close".localized) 91 | alert.beginSheetModal(for: window) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/String+JSON.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | /// - Returns: true if the JSON is valid, `false` otherwise. 5 | func validateJSON() throws { 6 | let jsonData = data(using: .utf8) ?? Data() 7 | do { 8 | try JSONSerialization.jsonObject(with: jsonData, options: []) 9 | } catch { 10 | let nsError = error as NSError 11 | var userInfo = nsError.userInfo 12 | userInfo[NSLocalizedDescriptionKey] = userInfo[NSDebugDescriptionErrorKey] 13 | throw NSError(domain: nsError.domain, code: nsError.code, userInfo: userInfo) 14 | } 15 | } 16 | 17 | var isDefaultPayload: Bool { 18 | [DefaultPayloads.apns, DefaultPayloads.fcmLegacy, DefaultPayloads.fcmV1].contains(self) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/String+Localization.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | var localized: String { 5 | guard let bundle = Bundle.sdkAssets else { 6 | return self 7 | } 8 | return NSLocalizedString(self, bundle: bundle, comment: "") 9 | } 10 | } 11 | 12 | private extension Bundle { 13 | static func bundle(bundleIdSubstring: String) -> Bundle? { 14 | (allBundles + allFrameworks).first(where: { $0.bundleIdentifier?.contains(bundleIdSubstring) == true }) 15 | } 16 | 17 | static var sdk: Bundle? { 18 | .bundle(bundleIdSubstring: "PusherMainView") 19 | } 20 | 21 | static var sdkAssets: Bundle? { 22 | if let resourceBundle = bundle(bundleIdSubstring: "PusherMainViewResources") { 23 | return resourceBundle 24 | } 25 | 26 | guard let sdkBundleURL = sdk?.resourceURL else { 27 | return nil 28 | } 29 | return .init(url: sdkBundleURL.appendingPathComponent("PusherMainViewResources.bundle")) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/String+Subscript.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | subscript (range: Range) -> String { 5 | let start = index(startIndex, offsetBy: range.lowerBound) 6 | let end = index(startIndex, offsetBy: range.upperBound) 7 | return String(self[start ..< end]) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/Timestamp.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Timestamp { 4 | /// - Returns: the current timestamp integer value. 5 | static var current: Int { 6 | Int(Date().timeIntervalSince1970) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainView/pushtypes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | alert 6 | background 7 | liveactivity 8 | 9 | 10 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainViewTests/DictionarySpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Quick 3 | import Nimble 4 | @testable import PusherMainView 5 | 6 | final class DictionarySpec: QuickSpec { 7 | override func spec() { 8 | describe("Dictionary") { 9 | describe("update(with:)") { 10 | it("should update the dictionary with the expected timestamp") { 11 | var apnsPayload: [String: Any] = ["aps": ["timestamp": 1669202700]] 12 | apnsPayload.update(with: 1669202821) 13 | 14 | let aps = apnsPayload["aps"] as? [String: Int] 15 | 16 | expect(aps?["timestamp"]).to(equal(1669202821)) 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainViewTests/InterfaceFactorySpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Quick 3 | import Nimble 4 | @testable import PusherMainView 5 | 6 | final class InterfaceFactorySpec: QuickSpec { 7 | override func spec() { 8 | describe("InterfaceFactory") { 9 | describe("create(for:)") { 10 | context("When the given type is PushTypesViewController") { 11 | it("should create PushTypesViewController instance") { 12 | let viewController = InterfaceFactory.create(for: "Pusher") 13 | expect(viewController).toNot(beNil()) 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainViewTests/Mocks.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APNS 3 | import FCM 4 | @testable import PusherMainView 5 | 6 | class APNSPusherMock: APNSPushable { 7 | private var result: Result 8 | var type: APNSPusherType 9 | var identity: SecIdentity? 10 | 11 | init(result: Result, type: APNSPusherType) { 12 | self.result = result 13 | self.type = type 14 | identity = nil 15 | } 16 | 17 | func pushToDevice(_ token: String, 18 | payload: [String: Any], 19 | withTopic topic: String?, 20 | priority: Int, 21 | collapseID: String?, 22 | inSandbox sandbox: Bool, 23 | pushType: String, 24 | completion: @escaping (Result) -> Void) { 25 | completion(result) 26 | } 27 | 28 | func pushToSimulator(payload: String, appBundleID bundleID: String, completion: @escaping (Result) -> Void) { 29 | completion(result) 30 | } 31 | } 32 | 33 | struct FCMPusherMock: FCMPushable { 34 | let result: Result 35 | 36 | func pushUsingLegacyEndpoint(_ token: String, 37 | payload: [String: Any], 38 | collapseID: String?, 39 | serverKey: String, 40 | completion: @escaping (Result) -> Void) { 41 | completion(result) 42 | } 43 | 44 | func pushUsingV1Endpoint(_ token: String, 45 | payload: [String: Any], 46 | collapseID: String?, 47 | serverKey: String, 48 | projectID: String, 49 | completion: @escaping (Result) -> Void) { 50 | completion(result) 51 | } 52 | } 53 | 54 | class RouterMock: Routing { 55 | var fileURL: URL! 56 | private(set) var lastMessage: String? 57 | 58 | func presentDevicesList(from fromViewController: NSViewController, pusherStore: PusherInteracting) { 59 | } 60 | 61 | func presentPushTypesList(from fromViewController: NSViewController, pusherStore: PusherInteracting) { 62 | } 63 | 64 | func presentAuthTokenAlert(from fromViewController: NSViewController, pusherStore: PusherInteracting) { 65 | } 66 | 67 | func show(message: String, window: NSWindow?) { 68 | lastMessage = message 69 | } 70 | 71 | func browseFiles(from fromViewController: NSViewController, completion: @escaping (URL) -> Void) { 72 | } 73 | 74 | func saveFileAs(from fromViewController: NSViewController, completion: @escaping (URL) -> Void) { 75 | completion(fileURL) 76 | } 77 | 78 | func dismiss(from fromViewController: NSViewController) { 79 | } 80 | } 81 | 82 | final class ObserverMock: NSObject { 83 | private(set) var pusherState: PusherState? 84 | private(set) var errorState: ErrorState? 85 | } 86 | 87 | extension ObserverMock: PusherInteractable { 88 | func newState(state: PusherState) { 89 | self.pusherState = state 90 | } 91 | 92 | func newErrorState(_ errorState: ErrorState) { 93 | self.errorState = errorState 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainViewTests/PlistFinderSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Quick 3 | import Nimble 4 | @testable import PusherMainView 5 | 6 | final class PlistFinderSpec: QuickSpec { 7 | override func spec() { 8 | describe("PlistFinder") { 9 | describe("model(for:and:)") { 10 | context("When plist is pushtypes and type is PushTypesViewController") { 11 | it("should return the array of expected push types") { 12 | let pushTypes = PlistFinder<[String]>.model(for: "pushtypes", and: PushTypesViewController.self) 13 | expect(pushTypes).to(equal(["alert", "background", "liveactivity"])) 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PusherMainView/PusherMainViewTests/TimestampSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Quick 3 | import Nimble 4 | @testable import PusherMainView 5 | 6 | final class TimestampSpec: QuickSpec { 7 | override func spec() { 8 | describe("Timestamp") { 9 | describe("current") { 10 | it("should return the current time integer value") { 11 | expect(Timestamp.current).to(equal(Int(Date().timeIntervalSince1970))) 12 | } 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Platform](https://img.shields.io/badge/Platform-macOS-black) 2 | ![Compatibility](https://img.shields.io/badge/Compatibility-macOS%20%3E%3D%2010.13-orange) 3 | ![Compatibility](https://img.shields.io/badge/Swift-5.0-orange.svg) 4 | ![License](https://img.shields.io/badge/License-MIT-lightgrey.svg) 5 | ![Build Status](https://app.bitrise.io/app/120aff9438a0a19e.svg?token=fX7evo54lwDdFSg5xQfkWg&branch=master) 6 | 7 | # The macOS Push Tester App 8 | 9 | The macOS Push Tester App allows you to send push notifications through APNS (Apple Push Notification Service) or FCM (Firebase Cloud Messaging) and receive them on a device or simulator/emulator. 10 | 11 | The macOS Push Tester App can also send push notifications to Live Activities on iOS devices (iOS >= 16.1). This feature only works with APNS token. 12 | 13 | Android emulators must enable the Google API for Google Play services. 14 | 15 | It can also get device tokens from any iPhone on the same wifi network. 16 | 17 | **Notice**: This app was created to be used by the Rakuten SDK team internally. Anyone is free to use it but please be aware that it is unsupported. 18 | 19 | ## How to build/run from source 20 | 21 | - 1) Run `pod install` from root folder 22 | - 2) Open pusher.xcworkspace* 23 | - 3) Build and run 24 | 25 | ## How to build with Fastlane 26 | 27 | ### Install fastlane 28 | - 1) Using RubyGems `sudo gem install fastlane -NV` (or simply `bundle install`) 29 | 30 | - 2) Alternatively using Homebrew `brew cask install fastlane` 31 | 32 | ### Run fastlane 33 | Run `fastlane ci` 34 | 35 | ## Make your iOS app discoverable by the macOS Push Tester App 36 | 37 | - 1) Add this class to your iOS app 38 | 39 | ```swift 40 | import Foundation 41 | import MultipeerConnectivity 42 | 43 | /// A device token can be generated from APNS or ActivityKit. 44 | public enum DeviceTokenType: String { 45 | case apns = "APNS" 46 | case activityKit = "ActivityKit" 47 | } 48 | 49 | public final class DeviceAdvertiser: NSObject { 50 | private var nearbyServiceAdvertiser: MCNearbyServiceAdvertiser? 51 | private let serviceType: String 52 | 53 | private enum Keys { 54 | static let deviceToken = "token" 55 | static let applicationIdentifier = "appID" 56 | static let deviceTokenType = "type" 57 | } 58 | 59 | public init(serviceType: String) { 60 | self.serviceType = serviceType 61 | super.init() 62 | } 63 | 64 | /// Start advertising peer with device token (token), app identifier (appID) and device token type (type). 65 | /// 66 | /// - Parameters: 67 | /// - deviceToken: The APNS or ActivityKit device token 68 | /// - type: the device token type (APNS or ActivityKit) 69 | public func setDeviceToken(_ deviceToken: String, 70 | type: DeviceTokenType = .apns) { 71 | if let advertiser = nearbyServiceAdvertiser { 72 | advertiser.stopAdvertisingPeer() 73 | } 74 | 75 | let peerID = MCPeerID(displayName: UIDevice.current.name) 76 | 77 | nearbyServiceAdvertiser = MCNearbyServiceAdvertiser( 78 | peer: peerID, 79 | discoveryInfo: [Keys.deviceToken: deviceToken, 80 | Keys.applicationIdentifier: Bundle.main.bundleIdentifier ?? "", 81 | Keys.deviceTokenType: type.rawValue], 82 | serviceType: serviceType 83 | ) 84 | 85 | nearbyServiceAdvertiser?.delegate = self 86 | nearbyServiceAdvertiser?.startAdvertisingPeer() 87 | } 88 | } 89 | 90 | extension DeviceAdvertiser: MCNearbyServiceAdvertiserDelegate { 91 | public func advertiser(_ advertiser: MCNearbyServiceAdvertiser, 92 | didReceiveInvitationFromPeer peerID: MCPeerID, 93 | withContext context: Data?, 94 | invitationHandler: @escaping (Bool, MCSession?) -> Void) { 95 | invitationHandler(false, MCSession()) 96 | } 97 | 98 | public func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) { 99 | } 100 | } 101 | ``` 102 | - 2) Instantiate `DeviceAdvertiser` 103 | 104 | ```swift 105 | let deviceAdvertiser = DeviceAdvertiser(serviceType: "pusher") 106 | ``` 107 | 108 | - 3) Set the device token 109 | 110 | ```swift 111 | func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 112 | deviceAdvertiser.setDeviceToken(deviceToken.hexadecimal) 113 | } 114 | ``` 115 | 116 | - 4) Add this `Data` extension to convert deviceToken to `String` 117 | 118 | ```swift 119 | import Foundation 120 | 121 | extension Data { 122 | var hexadecimal: String { 123 | map { String(format: "%02x", $0) }.joined() 124 | } 125 | } 126 | ``` 127 | 128 | - 5) Add the following to your targets info.plist (required for iOS 14 and above) 129 | 130 | ```xml 131 | NSBonjourServices 132 | 133 | _pusher._tcp 134 | _pusher._udp 135 | 136 | NSLocalNetworkUsageDescription 137 | To allow Pusher App to discover this device on the network. 138 | ``` 139 | 140 | ## UI Preview 141 | 142 | ![Send push notification to iOS device](preview-push-ios-device.png) 143 | ![Send push notification to iOS simulator](preview-push-ios-simulator.png) 144 | ![Send push notification to Android device](preview-push-android-device.png) 145 | 146 | -------------------------------------------------------------------------------- /bitrise.yml: -------------------------------------------------------------------------------- 1 | --- 2 | format_version: '11' 3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 4 | project_type: macos 5 | workflows: 6 | 7 | primary: 8 | before_run: 9 | - _setup 10 | steps: 11 | - fastlane@3: 12 | inputs: 13 | - update_fastlane: 'false' 14 | - lane: ci 15 | title: Build & Test 16 | - script@1: 17 | title: Export test results 18 | is_always_run: true 19 | inputs: 20 | - content: |- 21 | #!/usr/bin/env bash 22 | set -ex 23 | 24 | # Source: https://devcenter.bitrise.io/testing/exporting-to-test-reports-from-custom-script-steps/ 25 | 26 | JUNIT=./artifacts/unit-tests/report.junit 27 | if [ ! -f "$JUNIT" ]; then 28 | echo "No JUnit file to export" 29 | exit 0 30 | fi 31 | # Creating the sub-directory for the test run within the BITRISE_TEST_RESULT_DIR: 32 | test_run_dir="$BITRISE_TEST_RESULT_DIR/result_dir_1" 33 | mkdir "$test_run_dir" 34 | 35 | # Exporting the JUnit XML test report: 36 | cp "$JUNIT" "$test_run_dir/UnitTest.xml" 37 | 38 | # Creating the test-info.json file with the name of the test run defined: 39 | echo '{"test-name":"Tests scheme run"}' >> "$test_run_dir/test-info.json" 40 | - deploy-to-bitrise-io@2: 41 | run_if: '{{enveq "BITRISE_GIT_BRANCH" "master"}}' 42 | inputs: 43 | - deploy_path: ./artifacts 44 | - is_compress: 'true' 45 | - deploy-to-bitrise-io@2: 46 | title: 'Export test results only' 47 | run_if: '{{getenv "BITRISE_GIT_BRANCH" | ne "master"}}' 48 | - cache-push@2: 49 | inputs: 50 | - cache_paths: |- 51 | $BITRISE_CACHE_DIR 52 | $GEM_CACHE_PATH 53 | 54 | _setup: 55 | steps: 56 | - git-clone@6: 57 | inputs: 58 | - update_submodules: 'no' 59 | - merge_pr: 'no' 60 | - cache-pull@2: {} 61 | - script@1: 62 | title: Bundle install 63 | inputs: 64 | - content: |- 65 | #!/usr/bin/env bash 66 | set -ex 67 | if [ ! -f "Gemfile" ]; then 68 | echo "No Gemfile detected. Skipping..." 69 | exit 0 70 | fi 71 | bundle install 72 | RBENV_DIR="`cd $(rbenv which ruby)/../..;pwd`" 73 | echo "Gem cache directory: $RBENV_DIR" 74 | envman add --key GEM_CACHE_PATH --value $RBENV_DIR 75 | - brew-install@0: 76 | title: Install swiftlint 77 | inputs: 78 | - cache_enabled: 'yes' 79 | - upgrade: 'no' 80 | - packages: swiftlint 81 | - cocoapods-install@1: 82 | inputs: 83 | - verbose: 'false' 84 | 85 | app: 86 | envs: 87 | - opts: 88 | is_expand: false 89 | BITRISE_PROJECT_PATH: pusher.xcworkspace 90 | - opts: 91 | is_expand: false 92 | BITRISE_SCHEME: pusher 93 | - opts: 94 | is_expand: false 95 | BITRISE_EXPORT_METHOD: none 96 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:mac) 2 | 3 | platform :mac do 4 | desc "Building Pusher" 5 | lane :ci do 6 | sh "rm -rf ../artifacts" 7 | sh "mkdir -p ../DerivedData" 8 | 9 | cocoapods 10 | scan( 11 | clean: true, 12 | skip_build: true, 13 | output_directory: './artifacts/unit-tests', 14 | derived_data_path: "./DerivedData", 15 | scheme: 'pusher', 16 | code_coverage: true, 17 | xcodebuild_formatter: 'xcpretty', 18 | output_types: 'json-compilation-database,html,junit', 19 | output_files: 'compile_commands.json,report.html,report.junit') 20 | 21 | build_mac_app( 22 | workspace: "pusher.xcworkspace", 23 | scheme: "pusher", 24 | configuration: "Release", 25 | derived_data_path: "./DerivedData", 26 | skip_archive: true) 27 | 28 | sh "cp -R ../DerivedData/Build/Products/Release/PushTester.app ../artifacts" 29 | sh "cp -R ../DerivedData/Build/Products/Release/PushTester.app.dSYM ../artifacts" 30 | sh "rm -rf ../DerivedData" 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /preview-push-android-device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/macos-push-tester/07e54808eb1d5b585f212fb04c0d8b711b9c68c0/preview-push-android-device.png -------------------------------------------------------------------------------- /preview-push-ios-device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/macos-push-tester/07e54808eb1d5b585f212fb04c0d8b711b9c68c0/preview-push-ios-device.png -------------------------------------------------------------------------------- /preview-push-ios-simulator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/macos-push-tester/07e54808eb1d5b585f212fb04c0d8b711b9c68c0/preview-push-ios-simulator.png -------------------------------------------------------------------------------- /pusher/pusher.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3A590CDE2325DBC4005AA8BA /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A590CDD2325DBC4005AA8BA /* main.swift */; }; 11 | 3A590CE02325E42E005AA8BA /* Application.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3A590CDF2325E42E005AA8BA /* Application.xib */; }; 12 | 3ACF98FD2320E1BC00C40BB4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF98FC2320E1BC00C40BB4 /* AppDelegate.swift */; }; 13 | 3ACF99012320E1BD00C40BB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3ACF99002320E1BD00C40BB4 /* Assets.xcassets */; }; 14 | C993548A2851E1E400A9439A /* RLogger in Frameworks */ = {isa = PBXBuildFile; productRef = C99354892851E1E400A9439A /* RLogger */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 3A590CDD2325DBC4005AA8BA /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 19 | 3A590CDF2325E42E005AA8BA /* Application.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Application.xib; sourceTree = ""; }; 20 | 3AA665E723223EA40011F310 /* PusherMainView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PusherMainView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 3ACF98F92320E1BC00C40BB4 /* PushTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PushTester.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 3ACF98FC2320E1BC00C40BB4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23 | 3ACF99002320E1BD00C40BB4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | 3ACF99052320E1BD00C40BB4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 25 | 3ACF99062320E1BD00C40BB4 /* pusher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = pusher.entitlements; sourceTree = ""; }; 26 | /* End PBXFileReference section */ 27 | 28 | /* Begin PBXFrameworksBuildPhase section */ 29 | 3ACF98F62320E1BC00C40BB4 /* Frameworks */ = { 30 | isa = PBXFrameworksBuildPhase; 31 | buildActionMask = 2147483647; 32 | files = ( 33 | C993548A2851E1E400A9439A /* RLogger in Frameworks */, 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 3AA0C8072321FA580087E53E /* Frameworks */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | 3AA665E723223EA40011F310 /* PusherMainView.framework */, 44 | ); 45 | name = Frameworks; 46 | sourceTree = ""; 47 | }; 48 | 3ACF98F02320E1BC00C40BB4 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 3ACF98FB2320E1BC00C40BB4 /* pusher */, 52 | 3ACF98FA2320E1BC00C40BB4 /* Products */, 53 | 3AA0C8072321FA580087E53E /* Frameworks */, 54 | ); 55 | sourceTree = ""; 56 | }; 57 | 3ACF98FA2320E1BC00C40BB4 /* Products */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 3ACF98F92320E1BC00C40BB4 /* PushTester.app */, 61 | ); 62 | name = Products; 63 | sourceTree = ""; 64 | }; 65 | 3ACF98FB2320E1BC00C40BB4 /* pusher */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 3ACF98FC2320E1BC00C40BB4 /* AppDelegate.swift */, 69 | 3A590CDF2325E42E005AA8BA /* Application.xib */, 70 | 3ACF99002320E1BD00C40BB4 /* Assets.xcassets */, 71 | 3ACF99052320E1BD00C40BB4 /* Info.plist */, 72 | 3A590CDD2325DBC4005AA8BA /* main.swift */, 73 | 3ACF99062320E1BD00C40BB4 /* pusher.entitlements */, 74 | ); 75 | path = pusher; 76 | sourceTree = ""; 77 | }; 78 | /* End PBXGroup section */ 79 | 80 | /* Begin PBXNativeTarget section */ 81 | 3ACF98F82320E1BC00C40BB4 /* pusher */ = { 82 | isa = PBXNativeTarget; 83 | buildConfigurationList = 3ACF99092320E1BD00C40BB4 /* Build configuration list for PBXNativeTarget "pusher" */; 84 | buildPhases = ( 85 | 3ACF98F52320E1BC00C40BB4 /* Sources */, 86 | 3ACF98F62320E1BC00C40BB4 /* Frameworks */, 87 | 3ACF98F72320E1BC00C40BB4 /* Resources */, 88 | C9D347E12840E3C2002BA6A9 /* ShellScript */, 89 | ); 90 | buildRules = ( 91 | ); 92 | dependencies = ( 93 | ); 94 | name = pusher; 95 | packageProductDependencies = ( 96 | C99354892851E1E400A9439A /* RLogger */, 97 | ); 98 | productName = pusher; 99 | productReference = 3ACF98F92320E1BC00C40BB4 /* PushTester.app */; 100 | productType = "com.apple.product-type.application"; 101 | }; 102 | /* End PBXNativeTarget section */ 103 | 104 | /* Begin PBXProject section */ 105 | 3ACF98F12320E1BC00C40BB4 /* Project object */ = { 106 | isa = PBXProject; 107 | attributes = { 108 | LastSwiftUpdateCheck = 1300; 109 | LastUpgradeCheck = 1300; 110 | ORGANIZATIONNAME = Rakuten; 111 | TargetAttributes = { 112 | 3ACF98F82320E1BC00C40BB4 = { 113 | CreatedOnToolsVersion = 10.3; 114 | SystemCapabilities = { 115 | com.apple.Sandbox = { 116 | enabled = 0; 117 | }; 118 | }; 119 | }; 120 | }; 121 | }; 122 | buildConfigurationList = 3ACF98F42320E1BC00C40BB4 /* Build configuration list for PBXProject "pusher" */; 123 | compatibilityVersion = "Xcode 9.3"; 124 | developmentRegion = en; 125 | hasScannedForEncodings = 0; 126 | knownRegions = ( 127 | en, 128 | Base, 129 | ); 130 | mainGroup = 3ACF98F02320E1BC00C40BB4; 131 | packageReferences = ( 132 | C99354882851E1E400A9439A /* XCRemoteSwiftPackageReference "ios-sdkutils" */, 133 | ); 134 | productRefGroup = 3ACF98FA2320E1BC00C40BB4 /* Products */; 135 | projectDirPath = ""; 136 | projectRoot = ""; 137 | targets = ( 138 | 3ACF98F82320E1BC00C40BB4 /* pusher */, 139 | ); 140 | }; 141 | /* End PBXProject section */ 142 | 143 | /* Begin PBXResourcesBuildPhase section */ 144 | 3ACF98F72320E1BC00C40BB4 /* Resources */ = { 145 | isa = PBXResourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | 3ACF99012320E1BD00C40BB4 /* Assets.xcassets in Resources */, 149 | 3A590CE02325E42E005AA8BA /* Application.xib in Resources */, 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXResourcesBuildPhase section */ 154 | 155 | /* Begin PBXShellScriptBuildPhase section */ 156 | C9D347E12840E3C2002BA6A9 /* ShellScript */ = { 157 | isa = PBXShellScriptBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | ); 161 | inputFileListPaths = ( 162 | ); 163 | inputPaths = ( 164 | ); 165 | outputFileListPaths = ( 166 | ); 167 | outputPaths = ( 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | shellPath = /bin/sh; 171 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint --config ../.swiftlint.yml --strict\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 172 | }; 173 | /* End PBXShellScriptBuildPhase section */ 174 | 175 | /* Begin PBXSourcesBuildPhase section */ 176 | 3ACF98F52320E1BC00C40BB4 /* Sources */ = { 177 | isa = PBXSourcesBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | 3A590CDE2325DBC4005AA8BA /* main.swift in Sources */, 181 | 3ACF98FD2320E1BC00C40BB4 /* AppDelegate.swift in Sources */, 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | }; 185 | /* End PBXSourcesBuildPhase section */ 186 | 187 | /* Begin XCBuildConfiguration section */ 188 | 3ACF99072320E1BD00C40BB4 /* Debug */ = { 189 | isa = XCBuildConfiguration; 190 | buildSettings = { 191 | ALWAYS_SEARCH_USER_PATHS = NO; 192 | CLANG_ANALYZER_NONNULL = YES; 193 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 194 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 195 | CLANG_CXX_LIBRARY = "libc++"; 196 | CLANG_ENABLE_MODULES = YES; 197 | CLANG_ENABLE_OBJC_ARC = YES; 198 | CLANG_ENABLE_OBJC_WEAK = YES; 199 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 200 | CLANG_WARN_BOOL_CONVERSION = YES; 201 | CLANG_WARN_COMMA = YES; 202 | CLANG_WARN_CONSTANT_CONVERSION = YES; 203 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 204 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 205 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 206 | CLANG_WARN_EMPTY_BODY = YES; 207 | CLANG_WARN_ENUM_CONVERSION = YES; 208 | CLANG_WARN_INFINITE_RECURSION = YES; 209 | CLANG_WARN_INT_CONVERSION = YES; 210 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 211 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 212 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 213 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 214 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 215 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 216 | CLANG_WARN_STRICT_PROTOTYPES = YES; 217 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 218 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 219 | CLANG_WARN_UNREACHABLE_CODE = YES; 220 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 221 | CODE_SIGN_IDENTITY = "Mac Developer"; 222 | COPY_PHASE_STRIP = NO; 223 | DEBUG_INFORMATION_FORMAT = dwarf; 224 | ENABLE_STRICT_OBJC_MSGSEND = YES; 225 | ENABLE_TESTABILITY = YES; 226 | GCC_C_LANGUAGE_STANDARD = gnu11; 227 | GCC_DYNAMIC_NO_PIC = NO; 228 | GCC_NO_COMMON_BLOCKS = YES; 229 | GCC_OPTIMIZATION_LEVEL = 0; 230 | GCC_PREPROCESSOR_DEFINITIONS = ( 231 | "DEBUG=1", 232 | "$(inherited)", 233 | ); 234 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 235 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 236 | GCC_WARN_UNDECLARED_SELECTOR = YES; 237 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 238 | GCC_WARN_UNUSED_FUNCTION = YES; 239 | GCC_WARN_UNUSED_VARIABLE = YES; 240 | MACOSX_DEPLOYMENT_TARGET = 10.13; 241 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 242 | MTL_FAST_MATH = YES; 243 | ONLY_ACTIVE_ARCH = YES; 244 | SDKROOT = macosx; 245 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 246 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 247 | }; 248 | name = Debug; 249 | }; 250 | 3ACF99082320E1BD00C40BB4 /* Release */ = { 251 | isa = XCBuildConfiguration; 252 | buildSettings = { 253 | ALWAYS_SEARCH_USER_PATHS = NO; 254 | CLANG_ANALYZER_NONNULL = YES; 255 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 257 | CLANG_CXX_LIBRARY = "libc++"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_ARC = YES; 260 | CLANG_ENABLE_OBJC_WEAK = YES; 261 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 262 | CLANG_WARN_BOOL_CONVERSION = YES; 263 | CLANG_WARN_COMMA = YES; 264 | CLANG_WARN_CONSTANT_CONVERSION = YES; 265 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 266 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 267 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 268 | CLANG_WARN_EMPTY_BODY = YES; 269 | CLANG_WARN_ENUM_CONVERSION = YES; 270 | CLANG_WARN_INFINITE_RECURSION = YES; 271 | CLANG_WARN_INT_CONVERSION = YES; 272 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 273 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 274 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 275 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 276 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 277 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 278 | CLANG_WARN_STRICT_PROTOTYPES = YES; 279 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 280 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 281 | CLANG_WARN_UNREACHABLE_CODE = YES; 282 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 283 | CODE_SIGN_IDENTITY = "Mac Developer"; 284 | COPY_PHASE_STRIP = NO; 285 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 286 | ENABLE_NS_ASSERTIONS = NO; 287 | ENABLE_STRICT_OBJC_MSGSEND = YES; 288 | GCC_C_LANGUAGE_STANDARD = gnu11; 289 | GCC_NO_COMMON_BLOCKS = YES; 290 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 291 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 292 | GCC_WARN_UNDECLARED_SELECTOR = YES; 293 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 294 | GCC_WARN_UNUSED_FUNCTION = YES; 295 | GCC_WARN_UNUSED_VARIABLE = YES; 296 | MACOSX_DEPLOYMENT_TARGET = 10.13; 297 | MTL_ENABLE_DEBUG_INFO = NO; 298 | MTL_FAST_MATH = YES; 299 | SDKROOT = macosx; 300 | SWIFT_COMPILATION_MODE = wholemodule; 301 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 302 | }; 303 | name = Release; 304 | }; 305 | 3ACF990A2320E1BD00C40BB4 /* Debug */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 309 | CODE_SIGN_IDENTITY = "-"; 310 | CODE_SIGN_STYLE = Automatic; 311 | COMBINE_HIDPI_IMAGES = YES; 312 | DEVELOPMENT_TEAM = ""; 313 | INFOPLIST_FILE = pusher/Info.plist; 314 | LD_RUNPATH_SEARCH_PATHS = ( 315 | "$(inherited)", 316 | "@executable_path/../Frameworks", 317 | ); 318 | MACOSX_DEPLOYMENT_TARGET = 10.13; 319 | MARKETING_VERSION = "1.7.0-snapshot"; 320 | PRODUCT_BUNDLE_IDENTIFIER = com.rakuten.tech.mobile.pusher; 321 | PRODUCT_NAME = PushTester; 322 | SWIFT_VERSION = 5.0; 323 | }; 324 | name = Debug; 325 | }; 326 | 3ACF990B2320E1BD00C40BB4 /* Release */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 330 | CODE_SIGN_IDENTITY = "-"; 331 | CODE_SIGN_STYLE = Automatic; 332 | COMBINE_HIDPI_IMAGES = YES; 333 | DEVELOPMENT_TEAM = ""; 334 | INFOPLIST_FILE = pusher/Info.plist; 335 | LD_RUNPATH_SEARCH_PATHS = ( 336 | "$(inherited)", 337 | "@executable_path/../Frameworks", 338 | ); 339 | MACOSX_DEPLOYMENT_TARGET = 10.13; 340 | MARKETING_VERSION = "1.7.0-snapshot"; 341 | PRODUCT_BUNDLE_IDENTIFIER = com.rakuten.tech.mobile.pusher; 342 | PRODUCT_NAME = PushTester; 343 | SWIFT_VERSION = 5.0; 344 | }; 345 | name = Release; 346 | }; 347 | /* End XCBuildConfiguration section */ 348 | 349 | /* Begin XCConfigurationList section */ 350 | 3ACF98F42320E1BC00C40BB4 /* Build configuration list for PBXProject "pusher" */ = { 351 | isa = XCConfigurationList; 352 | buildConfigurations = ( 353 | 3ACF99072320E1BD00C40BB4 /* Debug */, 354 | 3ACF99082320E1BD00C40BB4 /* Release */, 355 | ); 356 | defaultConfigurationIsVisible = 0; 357 | defaultConfigurationName = Release; 358 | }; 359 | 3ACF99092320E1BD00C40BB4 /* Build configuration list for PBXNativeTarget "pusher" */ = { 360 | isa = XCConfigurationList; 361 | buildConfigurations = ( 362 | 3ACF990A2320E1BD00C40BB4 /* Debug */, 363 | 3ACF990B2320E1BD00C40BB4 /* Release */, 364 | ); 365 | defaultConfigurationIsVisible = 0; 366 | defaultConfigurationName = Release; 367 | }; 368 | /* End XCConfigurationList section */ 369 | 370 | /* Begin XCRemoteSwiftPackageReference section */ 371 | C99354882851E1E400A9439A /* XCRemoteSwiftPackageReference "ios-sdkutils" */ = { 372 | isa = XCRemoteSwiftPackageReference; 373 | repositoryURL = "https://github.com/rakutentech/ios-sdkutils"; 374 | requirement = { 375 | branch = master; 376 | kind = branch; 377 | }; 378 | }; 379 | /* End XCRemoteSwiftPackageReference section */ 380 | 381 | /* Begin XCSwiftPackageProductDependency section */ 382 | C99354892851E1E400A9439A /* RLogger */ = { 383 | isa = XCSwiftPackageProductDependency; 384 | package = C99354882851E1E400A9439A /* XCRemoteSwiftPackageReference "ios-sdkutils" */; 385 | productName = RLogger; 386 | }; 387 | /* End XCSwiftPackageProductDependency section */ 388 | }; 389 | rootObject = 3ACF98F12320E1BC00C40BB4 /* Project object */; 390 | } 391 | -------------------------------------------------------------------------------- /pusher/pusher.xcodeproj/xcshareddata/xcschemes/pusher.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /pusher/pusher/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import PusherMainView 3 | import RLogger 4 | 5 | final class AppDelegate: NSObject, NSApplicationDelegate { 6 | private var window: NSWindow? 7 | 8 | func applicationDidFinishLaunching(_ notification: Notification) { 9 | window = NSWindow(size: NSSize(width: 480, height: 480)) 10 | window?.contentViewController = PusherViewController.create() 11 | NSApplication.shared.loadMainMenu() 12 | } 13 | 14 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 15 | RLogger.debug(message: "App should terminate after last window closed") 16 | return true 17 | } 18 | 19 | func applicationWillTerminate(_ notification: Notification) { 20 | RLogger.debug(message: "App will terminate") 21 | } 22 | } 23 | 24 | private extension NSWindow { 25 | convenience init(size windowSize: NSSize) { 26 | let screenSize = NSScreen.main?.frame.size ?? .zero 27 | let rect = NSRect(x: screenSize.width/2 - windowSize.width/2, 28 | y: screenSize.height/2 - windowSize.height/2, 29 | width: windowSize.width, 30 | height: windowSize.height) 31 | self.init(contentRect: rect, 32 | styleMask: [.miniaturizable, .closable, .resizable, .titled], 33 | backing: .buffered, 34 | defer: false) 35 | minSize = windowSize 36 | makeKeyAndOrderFront(nil) 37 | } 38 | } 39 | 40 | private extension NSApplication { 41 | func loadMainMenu() { 42 | var topLevelObjects: NSArray? = [] 43 | Bundle.main.loadNibNamed("Application", owner: self, topLevelObjects: &topLevelObjects) 44 | mainMenu = topLevelObjects?.filter { $0 is NSMenu }.first as? NSMenu 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pusher/pusher/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /pusher/pusher/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /pusher/pusher/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2019 Rakuten. All rights reserved. 27 | NSPrincipalClass 28 | NSApplication 29 | LSApplicationCategory 30 | public.app-category.developer-tools 31 | 32 | 33 | -------------------------------------------------------------------------------- /pusher/pusher/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | let delegate = AppDelegate() 4 | NSApplication.shared.delegate = delegate 5 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 6 | -------------------------------------------------------------------------------- /pusher/pusher/pusher.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | --------------------------------------------------------------------------------