├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── swift.yml ├── .gitignore ├── .spi.yml ├── .swift-format ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── Dockerfile ├── ExampleApp ├── ExampleApp.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── ExampleApp.xcscheme │ │ ├── LocationPushServiceExtension.xcscheme │ │ └── NotificationServiceExtension.xcscheme ├── ExampleApp │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ExampleApp.entitlements │ ├── ExampleApp.swift │ ├── Info.plist │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Localizable.strings ├── LocationPushServiceExtension │ ├── Info.plist │ └── LocationPushService.swift └── NotificationServiceExtension │ ├── Info.plist │ └── NotificationService.swift ├── LICENSE ├── Package.swift ├── README.md ├── RELEASING.md ├── SECURITY.md ├── SUPPORT.md ├── Sources ├── APNS │ ├── APNS.docc │ │ └── APNSwift.md │ ├── APNSClient.swift │ ├── APNSConfiguration.swift │ └── Coding │ │ ├── APNSJSONDecoder.swift │ │ └── APNSJSONEncoder.swift ├── APNSCore │ ├── APNSAuthenticationTokenManager.swift │ ├── APNSClient.swift │ ├── APNSEnvironment.swift │ ├── APNSError.swift │ ├── APNSErrorResponse.swift │ ├── APNSMessage.swift │ ├── APNSNotificationExpiration.swift │ ├── APNSPriority.swift │ ├── APNSPushType.swift │ ├── APNSRequest.swift │ ├── APNSResponse.swift │ ├── Alert │ │ ├── APNSAlertNotification.swift │ │ ├── APNSAlertNotificationAPSStorage.swift │ │ ├── APNSAlertNotificationContent.swift │ │ ├── APNSAlertNotificationInterruptionLevel.swift │ │ ├── APNSAlertNotificationSound.swift │ │ └── APNSClient+Alert.swift │ ├── Background │ │ ├── APNSBackgroundNotification.swift │ │ └── APNSClient+Background.swift │ ├── Base64.swift │ ├── Complication │ │ ├── APNSClient+Complication.swift │ │ └── APNSComplicationNotification.swift │ ├── EmptyPayload.swift │ ├── FileProvider │ │ ├── APNSClient+FileProvider.swift │ │ └── APNSFileProviderNotification.swift │ ├── LiveActivity │ │ ├── APNSClient+LiveActivity.swift │ │ ├── APNSLiveActivityDismissalDate.swift │ │ ├── APNSLiveActivityNotification.swift │ │ ├── APNSLiveActivityNotificationAPSStorage.swift │ │ ├── APNSLiveActivityNotificationEvent.swift │ │ ├── APNSStartLiveActivityNotification.swift │ │ └── APNSStartLiveActivityNotificationAPSStorage.swift │ ├── Location │ │ ├── APNSClient+Location.swift │ │ └── APNSLocationNotification.swift │ ├── Logging.swift │ ├── P256.Signing.PrivateKey+PrivateFilePath.swift │ ├── PushToTalk │ │ ├── APNSClient+PushToTalk.swift │ │ └── APNSPushToTalkNotification.swift │ └── VoIP │ │ ├── APNSClient+VoIP.swift │ │ └── APNSVoIPNotification.swift ├── APNSExample │ └── Program.swift ├── APNSTestServer │ └── APNSTestServer.swift └── APNSURLSession │ ├── APNSURLSessionClientConfiguration.swift │ └── APNSUrlSessionClient.swift ├── Tests └── APNSTests │ ├── APNSAuthenticationTokenManagerTests.swift │ ├── APNSClientTests.swift │ ├── Alert │ └── APNSAlertNotificationTests.swift │ ├── Background │ └── APNSBackgroundNotificationTests.swift │ ├── Complication │ └── APNSComplicationNotificationTests.swift │ ├── FileProvider │ └── APNSFileProviderNotificationTests.swift │ ├── LiveActivity │ └── APNSLiveActivityNotificationTests.swift │ ├── PushToTalk │ └── APNSPushToTalkNotificationTests.swift │ └── VoIP │ └── APNSVoIPNotificationTests.swift └── scripts ├── generate_contributors_list.sh └── linux_test.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [kylebrowning] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | 27 | **Platform:** 28 | - OS: [e.g. Ubuntu] 29 | - Version [e.g. 16.04] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | - pull_request 4 | jobs: 5 | focal: 6 | container: 7 | image: swiftlang/swift:nightly-6.0-focal 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - run: swift test 12 | thread: 13 | container: 14 | image: swiftlang/swift:nightly-6.0-focal 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v1 18 | - run: swift test --sanitize=thread 19 | address: 20 | container: 21 | image: swiftlang/swift:nightly-6.0-focal 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v1 25 | - run: ASAN_OPTIONS=detect_leaks=0 swift test --sanitize=address 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | Package.pins 6 | *.pem 7 | /docs 8 | Package.resolved 9 | .podspecs 10 | DerivedData 11 | .swiftpm 12 | xcuserdata/ 13 | .vscode 14 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [APNS] 5 | scheme: APNS 6 | swift_version: 6.0 7 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentation" : { 6 | "spaces" : 4 7 | }, 8 | "indentConditionalCompilationBlocks" : true, 9 | "indentSwitchCaseLabels" : false, 10 | "lineBreakAroundMultilineExpressionChainComponents" : false, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : false, 13 | "lineBreakBeforeEachGenericRequirement" : false, 14 | "lineLength" : 100, 15 | "maximumBlankLines" : 1, 16 | "prioritizeKeepingFunctionOutputTogether" : false, 17 | "respectsExistingLineBreaks" : true, 18 | "rules" : { 19 | "AllPublicDeclarationsHaveDocumentation" : false, 20 | "AlwaysUseLowerCamelCase" : true, 21 | "AmbiguousTrailingClosureOverload" : true, 22 | "BeginDocumentationCommentWithOneLineSummary" : false, 23 | "DoNotUseSemicolons" : true, 24 | "DontRepeatTypeInStaticProperties" : true, 25 | "FileScopedDeclarationPrivacy" : true, 26 | "FullyIndirectEnum" : true, 27 | "GroupNumericLiterals" : true, 28 | "IdentifiersMustBeASCII" : true, 29 | "NeverForceUnwrap" : false, 30 | "NeverUseForceTry" : false, 31 | "NeverUseImplicitlyUnwrappedOptionals" : false, 32 | "NoAccessLevelOnExtensionDeclaration" : true, 33 | "NoBlockComments" : true, 34 | "NoCasesWithOnlyFallthrough" : true, 35 | "NoEmptyTrailingClosureParentheses" : true, 36 | "NoLabelsInCasePatterns" : true, 37 | "NoLeadingUnderscores" : false, 38 | "NoParensAroundConditions" : true, 39 | "NoVoidReturnOnFunctionSignature" : true, 40 | "OneCasePerLine" : true, 41 | "OneVariableDeclarationPerLine" : true, 42 | "OnlyOneTrailingClosureArgument" : true, 43 | "OrderedImports" : true, 44 | "ReturnVoidInsteadOfEmptyTuple" : true, 45 | "UseEarlyExits" : false, 46 | "UseLetInEveryBoundCaseVariable" : true, 47 | "UseShorthandTypeNames" : true, 48 | "UseSingleLinePropertyGetter" : true, 49 | "UseSynthesizedInitializer" : true, 50 | "UseTripleSlashForDocumentationComments" : true, 51 | "UseWhereClausesInForLoops" : false, 52 | "ValidateDocumentationComments" : false 53 | }, 54 | "tabWidth" : 4, 55 | "version" : 1 56 | } 57 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | To be a truly great community, APNSwift needs to welcome developers from all walks of life, 3 | with different backgrounds, and with a wide range of experience. A diverse and friendly 4 | community will have more great ideas, more unique perspectives, and produce more great 5 | code. We will work diligently to make the APNSwift community welcoming to everyone. 6 | 7 | To give clarity of what is expected of our members, APNSwift has adopted the code of conduct 8 | defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source 9 | communities, and we think it articulates our values well. The full text is copied below: 10 | 11 | ### Contributor Code of Conduct v1.3 12 | As contributors and maintainers of this project, and in the interest of fostering an open and 13 | welcoming community, we pledge to respect all people who contribute through reporting 14 | issues, posting feature requests, updating documentation, submitting pull requests or patches, 15 | and other activities. 16 | 17 | We are committed to making participation in this project a harassment-free experience for 18 | everyone, regardless of level of experience, gender, gender identity and expression, sexual 19 | orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or 20 | nationality. 21 | 22 | Examples of unacceptable behavior by participants include: 23 | - The use of sexualized language or imagery 24 | - Personal attacks 25 | - Trolling or insulting/derogatory comments 26 | - Public or private harassment 27 | - Publishing other’s private information, such as physical or electronic addresses, without explicit permission 28 | - Other unethical or unprofessional conduct 29 | 30 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 31 | commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of 32 | Conduct, or to ban temporarily or permanently any contributor for other behaviors that they 33 | deem inappropriate, threatening, offensive, or harmful. 34 | 35 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and 36 | consistently applying these principles to every aspect of managing this project. Project 37 | maintainers who do not follow or enforce the Code of Conduct may be permanently removed 38 | from the project team. 39 | 40 | This code of conduct applies both within project spaces and in public spaces when an 41 | individual is representing the project or its community. 42 | 43 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 44 | contacting a project maintainer. All complaints will be reviewed and 45 | investigated and will result in a response that is deemed necessary and appropriate to the 46 | circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter 47 | of an incident. 48 | 49 | *This policy is adapted from the Contributor Code of Conduct [version 1.3.0](https://contributor-covenant.org/version/1/3/0/).* 50 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Maintainers 2 | - [@kylebrowning](https://github.com/kylebrowning) 3 | 4 | ## Legal 5 | 6 | By submitting a pull request, you represent that you have the right to license your contribution to the community, and agree by submitting the patch 7 | that your contributions are licensed under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html) (see [`LICENSE`](../LICENSE)). 8 | 9 | ## Submitting a Bug 10 | 11 | Please ensure to specify the following: 12 | 13 | * APNSwift commit hash 14 | * Simplest possible steps to reproduce 15 | * A pull request with a failing test case is preferred, but it's just as fine to write it in the issue description 16 | * Environment Information 17 | * For example, are you running in Docker? How are you connecting to it through Docker? What version of Docker? 18 | * OS version and output of `uname -a` 19 | * Swift version or output of `swift --version` 20 | * output of `pkg-config --cflags openssl` 21 | 22 | ## Submitting a Pull Request 23 | 24 | A great PR that is likely to be merged quickly is: 25 | 26 | 1. Concise, with as few changes as needed to achieve the end result. 27 | 1. Tested, ensuring that regressions aren't introduced now or in the future. 28 | 1. Documented, adding API documentation as needed to cover new functions and properties. 29 | 1. Accompanied by a [great commit message](https://chris.beams.io/posts/git-commit/) 30 | 31 | # Contributor Conduct 32 | 33 | All contributors are expected to adhere to this project's [Code of Conduct](CODE_OF_CONDUCT.md). 34 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | For the purpose of tracking copyright, this is the list of individuals and 2 | organizations who have contributed source code to APNSwift. 3 | 4 | For employees of an organization/company where the copyright of work done 5 | by employees of that company is held by the company itself, only the company 6 | needs to be listed here. 7 | 8 | ## COPYRIGHT HOLDERS 9 | 10 | ### Contributors 11 | 12 | - Craig Newell 13 | - Eduardo Perez 14 | - Florian Reinhart 15 | - Franz Busch 16 | - Geoff Verdouw 17 | - Jeffrey Macko 18 | - Jonny 19 | - Kyle Browning 20 | - Laurent Gaches 21 | - Lukáš Petr 22 | - Mads Odgaard 23 | - Mark Woollard 24 | - Nikola Paunović 25 | - Roderic Campbell 26 | - Simon Kempendorf 27 | - Sven A. Schmidt 28 | - Tanner 29 | - Tanner Nelson 30 | - Tim Condon <0xTim@users.noreply.github.com> 31 | - Timothy Ellis <3098078+TimAEllis@users.noreply.github.com> 32 | - Timothy Ellis 33 | - Vojtech Rylko 34 | - grosch 35 | - itcohorts 36 | - tanner0101 37 | 38 | **Updating this list** 39 | 40 | Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM swift:5.0 2 | 3 | WORKDIR /code 4 | RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true && apt-get -q update && \ 5 | apt-get -q install -y \ 6 | openssl libssl-dev 7 | COPY Package.swift /code/. 8 | RUN swift package resolve 9 | COPY ./Sources /code/Sources 10 | COPY ./Tests /code/Tests 11 | 12 | RUN swift build -------------------------------------------------------------------------------- /ExampleApp/ExampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/LocationPushServiceExtension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 60 | 62 | 68 | 69 | 70 | 71 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp.xcodeproj/xcshareddata/xcschemes/NotificationServiceExtension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 60 | 62 | 68 | 69 | 70 | 71 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp/ExampleApp.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import PushKit 3 | import SwiftUI 4 | import UIKit 5 | 6 | @main 7 | struct ExampleApp: App { 8 | @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate 9 | @AppStorage("background-push") var receivedBackgroundPush: Bool = false 10 | @AppStorage("voip-push") var receivedVoIPPush: Bool = false 11 | 12 | var body: some Scene { 13 | WindowGroup { 14 | Text("Hello, world!") 15 | .padding() 16 | .onAppear { 17 | print("Appeared") 18 | } 19 | 20 | if UIApplication.shared.applicationIconBadgeNumber != 0 { 21 | VStack { 22 | Button("Clear badge") { 23 | UIApplication.shared.applicationIconBadgeNumber = 0 24 | } 25 | } 26 | } 27 | 28 | if receivedBackgroundPush { 29 | VStack { 30 | Text("Received background push") 31 | Button("Reset background push storage") { 32 | self.receivedBackgroundPush = false 33 | } 34 | } 35 | } 36 | if receivedVoIPPush { 37 | VStack { 38 | Text("Received VoIP push") 39 | Button("Reset VoIP push storage") { 40 | self.receivedVoIPPush = false 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | final class AppDelegate: NSObject, UIApplicationDelegate { 49 | var voipRegistry: PKPushRegistry! 50 | 51 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 52 | UNUserNotificationCenter.current().requestAuthorization( 53 | options: [.alert, .badge, .sound], 54 | completionHandler: { _, _ in } 55 | ) 56 | 57 | // Register for push notifications 58 | application.registerForRemoteNotifications() 59 | self.registerNotificationCategories() 60 | 61 | // Register for location update notifications 62 | let locationManager = CLLocationManager() 63 | locationManager.requestAlwaysAuthorization() 64 | Task { 65 | do { 66 | let deviceToken = try await locationManager.startMonitoringLocationPushes() 67 | print("Location token: \(deviceToken.asHexString)") 68 | } catch { 69 | print("Failed to start monitoring location pushes \(error)") 70 | } 71 | } 72 | 73 | // Register for VoIP notifications 74 | self.voipRegistry = PKPushRegistry(queue: nil) 75 | self.voipRegistry.delegate = self 76 | self.voipRegistry.desiredPushTypes = [.voIP] 77 | 78 | return true 79 | } 80 | 81 | func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 82 | print("Device token: \(deviceToken.asHexString)") 83 | } 84 | 85 | func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { 86 | print(error.localizedDescription) 87 | } 88 | 89 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], 90 | fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 91 | UserDefaults.standard.set(true, forKey: "background-push") 92 | completionHandler(.newData) 93 | } 94 | } 95 | 96 | extension AppDelegate { 97 | private func registerNotificationCategories() { 98 | let customAction = UNNotificationAction( 99 | identifier: "CUSTOM_ACTION", 100 | title: "Custom", 101 | options: [] 102 | ) 103 | let customCategory = UNNotificationCategory( 104 | identifier: "CUSTOM", 105 | actions: [customAction], 106 | intentIdentifiers: [], 107 | hiddenPreviewsBodyPlaceholder: "", 108 | options: .customDismissAction 109 | ) 110 | let notificationCenter = UNUserNotificationCenter.current() 111 | notificationCenter.setNotificationCategories([customCategory]) 112 | } 113 | } 114 | 115 | extension AppDelegate: PKPushRegistryDelegate { 116 | func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { 117 | print("Credentials for \(type): \(pushCredentials.token.asHexString)") 118 | } 119 | 120 | func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) async { 121 | UserDefaults.standard.set(true, forKey: "voip-push") 122 | } 123 | } 124 | 125 | extension Data { 126 | // Convenience method to convert `Data` to a hex `String`. 127 | fileprivate var asHexString: String { 128 | let hexString = map { String(format: "%02.2hhx", $0) }.joined() 129 | return hexString 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIBackgroundModes 6 | 7 | location 8 | remote-notification 9 | voip 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ExampleApp/ExampleApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ExampleApp/Localizable.strings: -------------------------------------------------------------------------------- 1 | "title" = "Title %@"; 2 | "subtitle" = "Subtitle %@"; 3 | "body" = "Body %@"; 4 | -------------------------------------------------------------------------------- /ExampleApp/LocationPushServiceExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.location.push.service 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).LocationPushService 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ExampleApp/LocationPushServiceExtension/LocationPushService.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | 3 | final class LocationPushService: NSObject, CLLocationPushServiceExtension, CLLocationManagerDelegate { 4 | var completion: (() -> Void)? 5 | var locationManager: CLLocationManager? 6 | 7 | func didReceiveLocationPushPayload(_ payload: [String: Any], completion: @escaping () -> Void) { 8 | self.completion = completion 9 | self.locationManager = CLLocationManager() 10 | self.locationManager!.delegate = self 11 | self.locationManager!.requestLocation() 12 | } 13 | 14 | func serviceExtensionWillTerminate() { 15 | self.completion?() 16 | } 17 | 18 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 19 | self.completion?() 20 | } 21 | 22 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 23 | self.completion?() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ExampleApp/NotificationServiceExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.usernotifications.service 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).NotificationService 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ExampleApp/NotificationServiceExtension/NotificationService.swift: -------------------------------------------------------------------------------- 1 | import UserNotifications 2 | 3 | final class NotificationService: UNNotificationServiceExtension { 4 | override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { 5 | let content = request.content.mutableCopy() as! UNMutableNotificationContent 6 | content.title = "Modified title" 7 | contentHandler(content) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "apnswift", 6 | platforms: [ 7 | .macOS(.v13), 8 | .iOS(.v16), 9 | .watchOS(.v9), 10 | .tvOS(.v16), 11 | ], 12 | products: [ 13 | .executable(name: "APNSExample", targets: ["APNSExample"]), 14 | .library(name: "APNS", targets: ["APNS"]), 15 | .library(name: "APNSCore", targets: ["APNSCore"]), 16 | .library(name: "APNSURLSession", targets: ["APNSURLSession"]), 17 | .library(name: "APNSTestServer", targets: ["APNSTestServer"]), 18 | ], 19 | dependencies: [ 20 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), 21 | .package(url: "https://github.com/apple/swift-crypto.git", "3.0.0" ..< "5.0.0"), 22 | .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.19.0"), 23 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 24 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.42.0"), 25 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.6.0"), 26 | .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.9.0"), 27 | ], 28 | targets: [ 29 | .executableTarget( 30 | name: "APNSExample", 31 | dependencies: [ 32 | .target(name: "APNSCore"), 33 | .target(name: "APNS"), 34 | .product(name: "Logging", package: "swift-log"), 35 | ] 36 | ), 37 | .testTarget( 38 | name: "APNSTests", 39 | dependencies: [ 40 | .target(name: "APNSCore"), 41 | .target(name: "APNS"), 42 | ] 43 | ), 44 | .target( 45 | name: "APNSCore", 46 | dependencies: [ 47 | .product(name: "Crypto", package: "swift-crypto"), 48 | ] 49 | ), 50 | .target( 51 | name: "APNS", 52 | dependencies: [ 53 | .product(name: "Crypto", package: "swift-crypto"), 54 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 55 | .target(name: "APNSCore"), 56 | ] 57 | ), 58 | .target( 59 | name: "APNSTestServer", 60 | dependencies: [ 61 | .product(name: "Logging", package: "swift-log"), 62 | .product(name: "Crypto", package: "swift-crypto"), 63 | .product(name: "NIOCore", package: "swift-nio"), 64 | .product(name: "NIOPosix", package: "swift-nio"), 65 | .product(name: "NIOSSL", package: "swift-nio-ssl"), 66 | .product(name: "NIOHTTP1", package: "swift-nio"), 67 | .product(name: "NIOHTTP2", package: "swift-nio-http2"), 68 | ] 69 | ), 70 | .target( 71 | name: "APNSURLSession", 72 | dependencies: [ 73 | .target(name: "APNSCore"), 74 | ] 75 | ), 76 | ], 77 | swiftLanguageModes: [.v6] 78 | ) 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![sswg:graduated|94x20](https://img.shields.io/badge/sswg-graduated-green.svg)]([https://github.com/swift-server/sswg/blob/master/process/incubation.md#sandbox-level](https://www.swift.org/sswg/incubation-process.html#graduation-requirements)) 2 | [![Build](https://github.com/kylebrowning/APNSwift/workflows/test/badge.svg)](https://github.com/kylebrowning/APNSwift/actions) 3 | [![Documentation](https://img.shields.io/badge/documentation-blueviolet.svg)](https://swiftpackageindex.com/swift-server-community/APNSwift/documentation) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswift-server-community%2FAPNSwift%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swift-server-community/APNSwift) 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswift-server-community%2FAPNSwift%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swift-server-community/APNSwift) 6 |

APNSwift

7 | 8 | A non-blocking Swift module for sending remote Apple Push Notification requests to [APNS](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server) built on AsyncHttpClient. 9 | 10 | - [Installation](#installation) 11 | - [Getting Started](#getting-started) 12 | - [Sending a simple notification](#sending-a-simple-notification) 13 | - [Sending Live Activity Update](#sending-live-activity-update--end) 14 | - [Authentication](#authentication) 15 | - [Logging](#logging) 16 | - [**Background Activity Logger**](#background-activity-logger) 17 | - [**Notification Send Logger**](#notification-send-logger) 18 | - [Server Example](#server-example) 19 | - [iOS Examples](#ios-examples) 20 | - [Original pitch and discussion on API](#original-pitch-and-discussion-on-api) 21 | 22 | ## Installation 23 | 24 | To install `APNSwift`, just add the package as a dependency in your [**Package.swift**](https://github.com/apple/swift-package-manager/blob/master/Documentation/PackageDescriptionV4.md#dependencies). 25 | 26 | ```swift 27 | dependencies: [ 28 | .package(url: "https://github.com/swift-server-community/APNSwift.git", from: "6.0.0"), 29 | ] 30 | ``` 31 | 32 | ## Getting Started 33 | APNSwift aims to provide semantically correct structures to sending push notifications. You first need to setup a [`APNSClient`](https://github.com/swift-server-community/APNSwift/blob/main/Sources/APNS/APNSClient.swift). To do that youll need to know your authentication method 34 | 35 | ```swift 36 | let client = APNSClient( 37 | configuration: .init( 38 | authenticationMethod: .jwt( 39 | privateKey: try .init(pemRepresentation: privateKey), 40 | keyIdentifier: keyIdentifier, 41 | teamIdentifier: teamIdentifier 42 | ), 43 | environment: .development 44 | ), 45 | eventLoopGroupProvider: .createNew, 46 | responseDecoder: JSONDecoder(), 47 | requestEncoder: JSONEncoder() 48 | ) 49 | 50 | // Shutdown the client when done 51 | try await client.shutdown() 52 | ``` 53 | 54 | ## Sending a simple notification 55 | All notifications require a payload, but that payload can be empty. Payload just needs to conform to `Encodable` 56 | 57 | ```swift 58 | struct Payload: Codable {} 59 | 60 | try await client.sendAlertNotification( 61 | .init( 62 | alert: .init( 63 | title: .raw("Simple Alert"), 64 | subtitle: .raw("Subtitle"), 65 | body: .raw("Body"), 66 | launchImage: nil 67 | ), 68 | expiration: .immediately, 69 | priority: .immediately, 70 | topic: "com.app.bundle", 71 | payload: Payload() 72 | ), 73 | deviceToken: "device-token" 74 | ) 75 | ``` 76 | 77 | ## Sending Live Activity Update / End 78 | It requires sending `ContentState` matching with the live activity configuration to successfully update activity state. `ContentState` needs to conform to `Encodable` and `Sendable`. 79 | 80 | ```swift 81 | try await client.sendLiveActivityNotification( 82 | .init( 83 | expiration: .immediately, 84 | priority: .immediately, 85 | appID: "com.app.bundle", 86 | contentState: ContentState, 87 | event: .update, 88 | timestamp: Int(Date().timeIntervalSince1970) 89 | ), 90 | deviceToken: activityPushToken 91 | ) 92 | ``` 93 | 94 | ```swift 95 | try await client.sendLiveActivityNotification( 96 | .init( 97 | expiration: .immediately, 98 | priority: .immediately, 99 | appID: "com.app.bundle", 100 | contentState: ContentState, 101 | event: .end, 102 | timestamp: Int(Date().timeIntervalSince1970), 103 | dismissalDate: .immediately // Optional to alter default behaviour 104 | ), 105 | deviceToken: activityPushToken 106 | ) 107 | ``` 108 | ## Authentication 109 | `APNSwift` provides two authentication methods. `jwt`, and `TLS`. 110 | 111 | **`jwt` is preferred and recommend by Apple** 112 | These can be configured when created your `APNSClientConfiguration` 113 | 114 | *Notes: `jwt` requires an encrypted version of your .p8 file from Apple which comes in a `pem` format. If you're having trouble with your key being invalid please confirm it is a PEM file* 115 | ``` 116 | openssl pkcs8 -nocrypt -in /path/to/my/key.p8 -out ~/Downloads/key.pem 117 | ``` 118 | 119 | ## Logging 120 | By default APNSwift has a no-op logger which will not log anything. However if you pass a logger in, you will see logs. 121 | 122 | There are currently two kinds of loggers. 123 | #### **Background Activity Logger** 124 | This logger can be passed into the `APNSClient` and will log background things like connection pooling, auth token refreshes, etc. 125 | 126 | #### **Notification Send Logger** 127 | This logger can be passed into any of the `send:` methods and will log everything related to a single send request. 128 | 129 | ## Server Example 130 | Take a look at [Program.swift](https://github.com/swift-server-community/APNSwift/blob/main/Sources/APNSExample/Program.swift) 131 | 132 | ## iOS Examples 133 | 134 | For an iOS example, open the example project within this repo. 135 | 136 | Once inside configure your App Bundle ID and assign your development team. Build and run the ExampleApp to iOS Simulator, grab your device token, and plug it in to server example above. Background the app and run Program.swift 137 | 138 | ## Original pitch and discussion on API 139 | 140 | * Pitch discussion: [Swift Server Forums](https://forums.swift.org/t/apple-push-notification-service-implementation-pitch/20193) 141 | * Proposal: [SSWG-0006](https://forums.swift.org/t/feedback-nioapns-nio-based-apple-push-notification-service/24393) 142 | * 5.0 breaking changings: [Swift Server Forums]([Blog post here on breaking changing](https://forums.swift.org/t/apnswift-5-0-0-beta-release/60075/3)) 143 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Updating code/Release workflow 2 | 3 | APNSwift follows a standard open source release process 4 | 5 | ## Issue and Pull Request Management 6 | 7 | The primary objective of managing Issues and Pull Requests (PRs) is to enable easy reference to them in the future and to ensure a clear record of when specific issues were addressed in a release. 8 | 9 | - **Creating GitHub Issues and Linking PRs:** Every significant task should have an associated GitHub issue, and when a PR resolves an issue, it should be linked using GitHub's "resolves #1234" mechanism or another clear indication of the associated issue. 10 | 11 | - **Closing Issues and PRs:** When a PR gets merged, the related issue is automatically closed, and the issue is assigned to the milestone corresponding to the release in which the change will be included. 12 | 13 | - **Handling PRs without Associated Issues:** In cases where a pull request is made directly without an associated issue, it should be linked to the relevant milestone for the release. However, it's essential not to assign both an issue and a pull request related to the same task to the same milestone, as this could lead to confusion regarding duplicated issue resolutions. 14 | 15 | ## Release Process 16 | 17 | When preparing for a new release, APNSWift will follow these steps. Let's use version `1.2.3` as an example: 18 | 19 | 1. Check all outstanding PRs, and if any can be merged for the current release (`1.2.3`), consider doing so. 20 | 21 | 2. Ensure that all recently closed PRs or issues are appropriately assigned to the milestone (`1.2.3`), if not already done. 22 | 23 | 3. Ensure all documentation is up to date 24 | 25 | 4. Create a new milestone for the next release, e.g., `1.2.4` or `1.3.0`, and move any remaining issues to it. This way, these tasks are carried over to the "next" release and can be easily located and prioritized. 26 | 27 | 5. Close the current milestone (`1.2.3`). 28 | 29 | 6. Finally, go to the GitHub releases page and [draft a new release](https://github.com/apple/swift-metrics/releases/new) with the details of the release version (`1.2.3` in this case) and any relevant release notes or changes. Be sure to include and create the new tag `1.2.3`. 30 | 31 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | APNSwift will support the current release minus 1 with security updates 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 5.x.x | :white_check_mark: | 10 | | 4.x.x | :white_check_mark: | 11 | | 3.x.x | ❌ | 12 | | 2.x.x | ❌ | 13 | | 1.x.x | ❌ | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Please open a security issue [here](https://github.com/kylebrowning/APNSwift/security/advisories) 18 | 19 | You can expect a response within 8 hours. 20 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | APNSwift will support the current release minus 1 with direct support. While the latest versions will get new features the N-1 release will get bug fixes and security fixes only. 2 | 3 | To receive suport, please [open an issue](https://github.com/swift-server-community/APNSwift/issues), [create a discussion](https://github.com/swift-server-community/APNSwift/discussions), or join [this](https://join.slack.com/t/swift-open-source/shared_invite/zt-203tkfk9g-rCNUZgj5kKhz9QW6Z9Gwqw) slack and enter the channel #apns. 4 | Please refer to the [Contributing](CONTRIBUTING.md) section for more information on how to contribute to the project. 5 | 6 | | Version | Supported | 7 | | ------- | ------------------ | 8 | | 5.x.x | :white_check_mark: | 9 | | 4.x.x | :white_check_mark: | 10 | | 3.x.x | ❌ | 11 | | 2.x.x | ❌ | 12 | | 1.x.x | ❌ | 13 | -------------------------------------------------------------------------------- /Sources/APNS/APNS.docc/APNSwift.md: -------------------------------------------------------------------------------- 1 | # ``APNS`` 2 | 3 | A non-blocking Swift module for sending remote Apple Push Notification requests to APNS built on AsyncHttpClient. 4 | 5 | ## Installation 6 | 7 | To install `APNSwift`, just add the package as a dependency in your [**Package.swift**](https://github.com/apple/swift-package-manager/blob/master/Documentation/PackageDescriptionV4.md#dependencies). 8 | 9 | ```swift 10 | dependencies: [ 11 | .package(url: "https://github.com/swift-server-community/APNSwift.git", from: "6.0.0"), 12 | ] 13 | ``` 14 | 15 | ## Getting Started 16 | APNSwift aims to provide semantically correct structures to sending push notifications. You first need to setup a [`APNSClient`](https://github.com/swift-server-community/APNSwift/blob/main/Sources/APNS/APNSClient.swift). To do that youll need to know your authentication method 17 | 18 | ```swift 19 | let client = APNSClient( 20 | configuration: .init( 21 | authenticationMethod: .jwt( 22 | privateKey: try .init(pemRepresentation: privateKey), 23 | keyIdentifier: keyIdentifier, 24 | teamIdentifier: teamIdentifier 25 | ), 26 | environment: .development 27 | ), 28 | eventLoopGroupProvider: .createNew, 29 | responseDecoder: JSONDecoder(), 30 | requestEncoder: JSONEncoder() 31 | ) 32 | 33 | // Shutdown the client when done 34 | try await client.shutdown() 35 | ``` 36 | 37 | ## Sending a simple notification 38 | All notifications require a payload, but that payload can be empty. Payload just needs to conform to `Encodable` 39 | 40 | ```swift 41 | struct Payload: Codable {} 42 | 43 | try await client.sendAlertNotification( 44 | .init( 45 | alert: .init( 46 | title: .raw("Simple Alert"), 47 | subtitle: .raw("Subtitle"), 48 | body: .raw("Body"), 49 | launchImage: nil 50 | ), 51 | expiration: .immediately, 52 | priority: .immediately, 53 | topic: "com.app.bundle", 54 | payload: Payload() 55 | ), 56 | deviceToken: "device-token" 57 | ) 58 | ``` 59 | 60 | ## Authentication 61 | `APNSwift` provides two authentication methods. `jwt`, and `TLS`. 62 | 63 | **`jwt` is preferred and recommend by Apple** 64 | These can be configured when created your `APNSClientConfiguration` 65 | 66 | *Notes: `jwt` requires an encrypted version of your .p8 file from Apple which comes in a `pem` format. If you're having trouble with your key being invalid please confirm it is a PEM file* 67 | ``` 68 | openssl pkcs8 -nocrypt -in /path/to/my/key.p8 -out ~/Downloads/key.pem 69 | ``` 70 | 71 | ## Logging 72 | By default APNSwift has a no-op logger which will not log anything. However if you pass a logger in, you will see logs. 73 | 74 | There are currently two kinds of loggers. 75 | #### **Background Activity Logger** 76 | This logger can be passed into the `APNSClient` and will log background things like connection pooling, auth token refreshes, etc. 77 | 78 | #### **Notification Send Logger** 79 | This logger can be passed into any of the `send:` methods and will log everything related to a single send request. 80 | 81 | ## Server Example 82 | Take a look at [Program.swift](https://github.com/swift-server-community/APNSwift/blob/main/Sources/APNSwiftExample/Program.swift) 83 | 84 | ## iOS Examples 85 | 86 | For an iOS example, open the example project within this repo. 87 | 88 | Once inside configure your App Bundle ID and assign your development team. Build and run the ExampleApp to iOS Simulator, grab your device token, and plug it in to server example above. Background the app and run Program.swift 89 | 90 | ## Original pitch and discussion on API 91 | 92 | * Pitch discussion: [Swift Server Forums](https://forums.swift.org/t/apple-push-notification-service-implementation-pitch/20193) 93 | * Proposal: [SSWG-0006](https://forums.swift.org/t/feedback-nioapns-nio-based-apple-push-notification-service/24393) 94 | * 5.0 breaking changings: [Swift Server Forums]([Blog post here on breaking changing](https://forums.swift.org/t/apnswift-5-0-0-beta-release/60075/3)) 95 | -------------------------------------------------------------------------------- /Sources/APNS/APNSClient.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | import AsyncHTTPClient 17 | import struct Foundation.Date 18 | import struct Foundation.UUID 19 | import NIOConcurrencyHelpers 20 | import NIOCore 21 | import NIOHTTP1 22 | import NIOSSL 23 | import NIOTLS 24 | import NIOPosix 25 | 26 | /// A client to talk with the Apple Push Notification services. 27 | public final class APNSClient: APNSClientProtocol { 28 | 29 | /// The configuration used by the ``APNSClient``. 30 | private let configuration: APNSClientConfiguration 31 | 32 | /// The ``HTTPClient`` used by the APNS. 33 | private let httpClient: HTTPClient 34 | 35 | /// The decoder for the responses from APNs. 36 | private let responseDecoder: Decoder 37 | 38 | /// The encoder for the requests to APNs. 39 | @usableFromInline 40 | /* private */ internal let requestEncoder: Encoder 41 | 42 | /// The authentication token manager. 43 | private let authenticationTokenManager: APNSAuthenticationTokenManager? 44 | 45 | /// The ByteBufferAllocator 46 | @usableFromInline 47 | /* private */ internal let byteBufferAllocator: ByteBufferAllocator 48 | 49 | /// Default ``HTTPHeaders`` which will be adapted for each request. This saves some allocations. 50 | private let defaultRequestHeaders: HTTPHeaders = { 51 | var headers = HTTPHeaders() 52 | headers.reserveCapacity(10) 53 | headers.add(name: "content-type", value: "application/json") 54 | headers.add(name: "user-agent", value: "APNS/swift-nio") 55 | return headers 56 | }() 57 | 58 | /// Initializes a new APNS. 59 | /// 60 | /// The client will create an internal `HTTPClient` which is used to make requests to APNs. 61 | /// This `HTTPClient` is intentionally internal since both authentication mechanisms are bound to a 62 | /// single connection and these connections cannot be shared. 63 | /// 64 | /// 65 | /// - Parameters: 66 | /// - configuration: The configuration used by the APNS. 67 | /// - eventLoopGroupProvider: Specify how EventLoopGroup will be created. 68 | /// - responseDecoder: The decoder for the responses from APNs. 69 | /// - requestEncoder: The encoder for the requests to APNs. 70 | /// - byteBufferAllocator: The `ByteBufferAllocator`. 71 | public init( 72 | configuration: APNSClientConfiguration, 73 | eventLoopGroupProvider: NIOEventLoopGroupProvider, 74 | responseDecoder: Decoder, 75 | requestEncoder: Encoder, 76 | byteBufferAllocator: ByteBufferAllocator = .init() 77 | ) { 78 | self.configuration = configuration 79 | self.byteBufferAllocator = byteBufferAllocator 80 | self.responseDecoder = responseDecoder 81 | self.requestEncoder = requestEncoder 82 | 83 | var tlsConfiguration = TLSConfiguration.makeClientConfiguration() 84 | switch configuration.authenticationMethod.method { 85 | case .jwt(let privateKey, let teamIdentifier, let keyIdentifier): 86 | self.authenticationTokenManager = APNSAuthenticationTokenManager( 87 | privateKey: privateKey, 88 | teamIdentifier: teamIdentifier, 89 | keyIdentifier: keyIdentifier, 90 | clock: ContinuousClock() 91 | ) 92 | case .tls(let privateKey, let certificateChain): 93 | self.authenticationTokenManager = nil 94 | tlsConfiguration.privateKey = privateKey 95 | tlsConfiguration.certificateChain = certificateChain 96 | } 97 | 98 | var httpClientConfiguration = HTTPClient.Configuration() 99 | httpClientConfiguration.tlsConfiguration = tlsConfiguration 100 | httpClientConfiguration.httpVersion = .automatic 101 | httpClientConfiguration.proxy = configuration.proxy 102 | 103 | switch eventLoopGroupProvider { 104 | case .shared(let eventLoopGroup): 105 | self.httpClient = HTTPClient( 106 | eventLoopGroupProvider: .shared(eventLoopGroup), 107 | configuration: httpClientConfiguration 108 | ) 109 | case .createNew: 110 | self.httpClient = HTTPClient( 111 | configuration: httpClientConfiguration 112 | ) 113 | } 114 | } 115 | 116 | /// Shuts down the client gracefully. 117 | public func shutdown() async throws { 118 | try await self.httpClient.shutdown() 119 | } 120 | } 121 | 122 | extension APNSClient: Sendable where Decoder: Sendable, Encoder: Sendable {} 123 | 124 | // MARK: - Raw sending 125 | 126 | extension APNSClient { 127 | 128 | public func send(_ request: APNSCore.APNSRequest) async throws -> APNSCore.APNSResponse { 129 | var headers = self.defaultRequestHeaders 130 | 131 | // Push type 132 | headers.add(name: "apns-push-type", value: request.pushType.description) 133 | 134 | // APNS ID 135 | if let apnsID = request.apnsID { 136 | headers.add(name: "apns-id", value: apnsID.uuidString.lowercased()) 137 | } 138 | 139 | // Expiration 140 | if let expiration = request.expiration?.expiration { 141 | headers.add(name: "apns-expiration", value: String(expiration)) 142 | } 143 | 144 | // Priority 145 | if let priority = request.priority?.rawValue { 146 | headers.add(name: "apns-priority", value: String(priority)) 147 | } 148 | 149 | // Topic 150 | if let topic = request.topic { 151 | headers.add(name: "apns-topic", value: topic) 152 | } 153 | 154 | // Collapse ID 155 | if let collapseID = request.collapseID { 156 | headers.add(name: "apns-collapse-id", value: collapseID) 157 | } 158 | 159 | // Authorization token 160 | if let authenticationTokenManager = self.authenticationTokenManager { 161 | let token = try await authenticationTokenManager.nextValidToken 162 | headers.add(name: "authorization", value: token) 163 | } 164 | 165 | // Device token 166 | let requestURL = "\(self.configuration.environment.absoluteURL)/\(request.deviceToken)" 167 | var byteBuffer = self.byteBufferAllocator.buffer(capacity: 0) 168 | 169 | try self.requestEncoder.encode(request.message, into: &byteBuffer) 170 | 171 | var httpClientRequest = HTTPClientRequest(url: requestURL) 172 | httpClientRequest.method = .POST 173 | httpClientRequest.headers = headers 174 | httpClientRequest.body = .bytes(byteBuffer) 175 | 176 | let response = try await self.httpClient.execute(httpClientRequest, deadline: .distantFuture) 177 | 178 | let apnsID = response.headers.first(name: "apns-id").flatMap { UUID(uuidString: $0) } 179 | let apnsUniqueID = response.headers.first(name: "apns-unique-id").flatMap { UUID(uuidString: $0) } 180 | 181 | if response.status == .ok { 182 | return APNSResponse(apnsID: apnsID, apnsUniqueID: apnsUniqueID) 183 | } 184 | 185 | let body = try await response.body.collect(upTo: 1024) 186 | let errorResponse = try responseDecoder.decode(APNSErrorResponse.self, from: body) 187 | 188 | let error = APNSError( 189 | responseStatus: Int(response.status.code), 190 | apnsID: apnsID, 191 | apnsUniqueID: apnsUniqueID, 192 | apnsResponse: errorResponse, 193 | timestamp: errorResponse.timestampInSeconds.flatMap { Date(timeIntervalSince1970: $0) } 194 | ) 195 | 196 | throw error 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Sources/APNS/APNSConfiguration.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | @preconcurrency import Crypto 17 | import NIOSSL 18 | import NIOTLS 19 | import AsyncHTTPClient 20 | 21 | /// The configuration of an ``APNSClient``. 22 | public struct APNSClientConfiguration: Sendable { 23 | /// The authentication method used by the ``APNSClient``. 24 | public struct AuthenticationMethod: Sendable { 25 | internal enum Method : Sendable{ 26 | case jwt(privateKey: P256.Signing.PrivateKey, teamIdentifier: String, keyIdentifier: String) 27 | case tls(privateKey: NIOSSLPrivateKeySource, certificateChain: [NIOSSLCertificateSource]) 28 | } 29 | 30 | /// Token-based authentication method. 31 | /// 32 | /// This authentication method is bound to a single connection since APNs will reject connections 33 | /// that use tokens signed by different keys. 34 | /// 35 | /// - Parameters: 36 | /// - privateKey: The private encryption key obtained through the developer portal. 37 | /// - keyIdentifier: The private encryption key identifier obtained through the developer portal. 38 | /// - teamIdentifier: The team id. 39 | public static func jwt( 40 | privateKey: P256.Signing.PrivateKey, keyIdentifier: String, 41 | teamIdentifier: String 42 | ) -> Self { 43 | Self(method: .jwt(privateKey: privateKey, teamIdentifier: teamIdentifier, keyIdentifier: keyIdentifier)) 44 | } 45 | 46 | /// Certificate based authentication method. 47 | /// 48 | /// - Parameters: 49 | /// - privateKey: The private key associated with the leaf certificate. 50 | /// - certificateChain: The certificates to offer during negotiation. If not present, no certificates will be offered. 51 | public static func tls( 52 | privateKey: NIOSSLPrivateKeySource, 53 | certificateChain: [NIOSSLCertificateSource] 54 | ) -> Self { 55 | Self(method: .tls(privateKey: privateKey, certificateChain: certificateChain)) 56 | } 57 | 58 | internal var method: Method 59 | } 60 | 61 | /// The authentication method used by the ``APNSClient``. 62 | public var authenticationMethod: AuthenticationMethod 63 | 64 | /// The environment used by the ``APNSClient``. 65 | public var environment: APNSEnvironment 66 | 67 | /// Upstream proxy, defaults to no proxy. 68 | public var proxy: HTTPClient.Configuration.Proxy? 69 | 70 | /// Initializes a new ``APNSClientConfiguration``. 71 | /// 72 | /// - Parameters: 73 | /// - authenticationMethod: The authentication method used by the ``APNSClient``. 74 | /// - environment: The environment used by the ``APNSClient``. 75 | public init( 76 | authenticationMethod: AuthenticationMethod, 77 | environment: APNSEnvironment 78 | ) { 79 | self.authenticationMethod = authenticationMethod 80 | self.environment = environment 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/APNS/Coding/APNSJSONDecoder.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import NIOCore 17 | import NIOFoundationCompat 18 | 19 | /// A protocol that is similar to the `JSONDecoder`. This allows users of APNSwift to customize the decoder used 20 | /// for decoding the APNS response bodies. 21 | public protocol APNSJSONDecoder { 22 | func decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T 23 | } 24 | 25 | extension JSONDecoder: APNSJSONDecoder { 26 | public func decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T { 27 | var copy = buffer 28 | let data = copy.readData(length: buffer.readableBytes)! 29 | return try self.decode(type, from: data) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/APNS/Coding/APNSJSONEncoder.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import NIOCore 17 | import NIOFoundationCompat 18 | 19 | /// A protocol that is similar to the `JSONEncoder`. This allows users of APNSwift to customize the encoder used 20 | /// for encoding the notification JSON payloads. 21 | public protocol APNSJSONEncoder { 22 | func encode(_ value: T, into buffer: inout ByteBuffer) throws 23 | } 24 | 25 | extension JSONEncoder: APNSJSONEncoder { 26 | public func encode(_ value: T, into buffer: inout ByteBuffer) throws { 27 | let data = try encode(value) 28 | buffer.writeData(data) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/APNSCore/APNSAuthenticationTokenManager.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Crypto 16 | import Dispatch 17 | 18 | /// A class to manage the authentication tokens for a single APNS connection. 19 | public final actor APNSAuthenticationTokenManager where Clock.Duration == Duration { 20 | private struct Token { 21 | /// This is the actual JWT token prefixed with `bearer`. 22 | /// 23 | /// This is stored as a ``String`` since we use it as an HTTP headers. 24 | var token: String 25 | var issuedAt: Clock.Instant 26 | } 27 | 28 | /// APNS rejects any token that is more than 1 hour old. We set the duration to be slightly less to refresh earlier. 29 | private let expirationDurationInSeconds: Duration = .seconds(60 * 55) 30 | 31 | /// The private key used for signing the tokens. 32 | private let privateKey: P256.Signing.PrivateKey 33 | /// The private key's team identifier. 34 | private let teamIdentifier: String 35 | /// The private key's identifier. 36 | private let keyIdentifier: String 37 | 38 | /// A closure to get the current time. This allows for properly testing the behaviour. 39 | /// Furthermore, we can expose this to clients at some point if they want to provide an NTP synced date. 40 | private let clock: Clock 41 | 42 | /// The last generated token. 43 | private var lastGeneratedToken: Token? 44 | 45 | /// Initializes a new ``APNSAuthenticationTokenManager``. 46 | /// 47 | /// - Parameters: 48 | /// - privateKey: The private key used for signing the tokens. 49 | /// - teamIdentifier: The private key's team identifier. 50 | /// - keyIdentifier: The private key's identifier. 51 | /// - logger: The logger. 52 | /// - currentTimeFactory: A closure to get the current time. 53 | public init( 54 | privateKey: P256.Signing.PrivateKey, 55 | teamIdentifier: String, 56 | keyIdentifier: String, 57 | clock: Clock 58 | ) { 59 | self.privateKey = privateKey 60 | self.teamIdentifier = teamIdentifier 61 | self.keyIdentifier = keyIdentifier 62 | self.clock = clock 63 | } 64 | 65 | /// This returns the next valid token. 66 | /// 67 | /// If there is a previously generated token that is still valid it will be returned, otherwise a fresh token will be generated. 68 | public var nextValidToken: String { 69 | get throws { 70 | /// First we check if there is a previously generated token 71 | /// and if that token is still valid. 72 | if let lastGeneratedToken = lastGeneratedToken, 73 | lastGeneratedToken.issuedAt.duration(to: self.clock.now) < .seconds(60 * 55) { 74 | /// The last generated token is still valid 75 | return lastGeneratedToken.token 76 | } else { 77 | let token = try generateNewToken( 78 | privateKey: privateKey, 79 | teamIdentifier: teamIdentifier, 80 | keyIdentifier: keyIdentifier 81 | ) 82 | lastGeneratedToken = token 83 | 84 | return token.token 85 | } 86 | } 87 | } 88 | 89 | private func generateNewToken( 90 | privateKey: P256.Signing.PrivateKey, 91 | teamIdentifier: String, 92 | keyIdentifier: String 93 | ) throws -> Token { 94 | let header = """ 95 | { 96 | "alg": "ES256", 97 | "typ": "JWT", 98 | "kid": "\(keyIdentifier)" 99 | } 100 | """ 101 | 102 | let issueAtTime = DispatchWallTime.now() 103 | let payload = """ 104 | { 105 | "iss": "\(teamIdentifier)", 106 | "iat": "\(issueAtTime.asSecondsSince1970)", 107 | "kid": "\(keyIdentifier)" 108 | } 109 | """ 110 | 111 | // The header and the payload need to be base64 encoded 112 | // before we can sign them 113 | let encodedHeader = Base64.encodeBytes(bytes: header.utf8, options: [.base64UrlAlphabet, .omitPaddingCharacter]) 114 | let encodedPayload = Base64.encodeBytes( 115 | bytes: payload.utf8, 116 | options: [.base64UrlAlphabet, .omitPaddingCharacter] 117 | ) 118 | let period = UInt8(ascii: ".") 119 | 120 | var encodedData = [UInt8]() 121 | /// This should fit the whole JWT token. I arrived at the number 122 | /// by generating a bunch of tokens and took the upper limit + some. 123 | encodedData.reserveCapacity(400) 124 | encodedData.append(contentsOf: encodedHeader) 125 | encodedData.append(period) 126 | encodedData.append(contentsOf: encodedPayload) 127 | 128 | let signatureData = try privateKey.signature(for: encodedData) 129 | let base64Signature = Base64.encodeBytes( 130 | bytes: signatureData.rawRepresentation, 131 | options: [.base64UrlAlphabet, .omitPaddingCharacter] 132 | ) 133 | 134 | encodedData.append(period) 135 | encodedData.append(contentsOf: base64Signature) 136 | 137 | // We are prefixing the token here to avoid an additional 138 | // allocation for setting the header. 139 | return Token( 140 | token: "bearer " + String(decoding: encodedData, as: UTF8.self), 141 | issuedAt: clock.now 142 | ) 143 | } 144 | } 145 | 146 | extension DispatchWallTime { 147 | internal var asSecondsSince1970: Int64 { 148 | -Int64(bitPattern: rawValue) / 1_000_000_000 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Sources/APNSCore/APNSClient.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public protocol APNSClientProtocol { 16 | func send(_ request: APNSRequest) async throws -> APNSResponse 17 | func shutdown() async throws 18 | } 19 | -------------------------------------------------------------------------------- /Sources/APNSCore/APNSEnvironment.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// The APNs environment. 16 | public struct APNSEnvironment: Sendable { 17 | /// The production APNs environment. 18 | public static let production = Self(url: "https://api.push.apple.com", port: 443) 19 | 20 | /// The sandbox APNs environment. 21 | @available(*, deprecated, renamed: "development") 22 | public static let sandbox = development 23 | 24 | /// The development APNs environment. 25 | public static let development = Self(url: "https://api.development.push.apple.com", port: 443) 26 | 27 | /// Creates an APNs environment with a custom URL. 28 | /// 29 | /// - Note: This is mostly used for testing purposes. 30 | public static func custom(url: String, port: Int = 443) -> Self { 31 | Self(url: url, port: port) 32 | } 33 | 34 | /// The environment's URL. 35 | public let url: String 36 | 37 | /// The environment's port. 38 | public let port: Int 39 | 40 | /// The fully constructed URL. 41 | public var absoluteURL: String { 42 | "\(url):\(port)/3/device" 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/APNSCore/APNSErrorResponse.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// A struct for the error response of APNs. 16 | /// 17 | /// This is just used to decode the JSON and should not be exposed. 18 | public struct APNSErrorResponse: Codable, Sendable { 19 | /// The error code indicating the reason for the failure. 20 | public var reason: String 21 | 22 | /// The time, represented in milliseconds since Epoch, at which APNs confirmed the token was no longer valid for the topic. 23 | /// This key is included only when the error in the `:status` field is `410`. 24 | public var timestamp: Int? 25 | 26 | /// The time, represented in seconds since Epoch, at which APNs confirmed the token was no longer valid for the topic. 27 | public var timestampInSeconds: Double? { 28 | self.timestamp.flatMap { Double($0) / 1000 } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/APNSCore/APNSMessage.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public protocol APNSMessage: Encodable, Sendable {} 16 | -------------------------------------------------------------------------------- /Sources/APNSCore/APNSNotificationExpiration.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// A struct representing the different expiration options for a notification. 16 | public struct APNSNotificationExpiration: Encodable, Hashable, Sendable { 17 | /// The date at which the notification is no longer valid. 18 | /// This value is a UNIX epoch expressed in seconds (UTC) 19 | public let expiration: Int? 20 | 21 | /// Omits sending an expiration for APNs. APNs will default to a default value. 22 | public static let none = Self(expiration: nil) 23 | 24 | /// This tells APNs to not try to redeliver the notification. 25 | /// 26 | /// - Important: This does not mean that the notification is delivered immediately. Due to various network conditions 27 | /// the message might be delivered with some delay. 28 | public static let immediately = Self(expiration: 0) 29 | 30 | /// This tells APNs that the notification expires at the given date. 31 | /// 32 | /// - Important: This does not mean that the notification is delivered until this date. Due to various network conditions 33 | /// the message might be delivered after the passed date. 34 | public static func timeIntervalSince1970InSeconds(_ timeInterval: Int) -> Self { 35 | Self(expiration: timeInterval) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/APNSCore/APNSPriority.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// A struct which represents the supported priorities by APNs. 16 | public struct APNSPriority: Hashable, Encodable, Sendable { 17 | /// The underlying raw value that is send to APNs. 18 | public let rawValue: Int 19 | 20 | /// Specifies that the notification should be send immediately. 21 | public static let immediately = Self(rawValue: 10) 22 | 23 | /// Specifies that the notification should be send based on power considerations on the user’s device. 24 | public static let consideringDevicePower = Self(rawValue: 5) 25 | } 26 | -------------------------------------------------------------------------------- /Sources/APNSCore/APNSPushType.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// A struct which represents the different supported APNs push types. 16 | public struct APNSPushType: Hashable, Sendable, CustomStringConvertible { 17 | 18 | internal enum Configuration: String, Hashable, Sendable { 19 | case alert 20 | case background 21 | case location 22 | case voip 23 | case complication 24 | case fileprovider 25 | case mdm 26 | case liveactivity 27 | case pushtotalk 28 | } 29 | 30 | public var description: String { 31 | configuration.rawValue 32 | } 33 | 34 | /// The underlying raw value that is send to APNs. 35 | internal var configuration: Configuration 36 | 37 | /// Use the alert push type for notifications that trigger a user interaction—for example, an alert, badge, or sound. 38 | /// 39 | /// If the notification requires immediate action from the user, set notification priority to `10`; otherwise use `5`. 40 | /// 41 | /// The alert push type is required on watchOS 6 and later. It is recommended on macOS, iOS, tvOS, and iPadOS. 42 | /// 43 | /// - Important: If you set this push type, the topic must use your app’s bundle ID as the topic. 44 | public static let alert = Self(configuration: .alert) 45 | 46 | /// Use the background push type for notifications that deliver content in the background, and don’t trigger any user interactions. 47 | /// 48 | /// The background push type is required on watchOS 6 and later. It is recommended on macOS, iOS, tvOS, and iPadOS. 49 | /// 50 | /// - Important: If you set this push type, the topic must use your app’s bundle ID as the topic. 51 | /// Always use priority `5`. Using priority `10` is an error. 52 | public static let background = Self(configuration: .background) 53 | 54 | /// Use the location push type for notifications that request a user’s location. 55 | /// 56 | /// If the location query requires an immediate response from the Location Push Service Extension, set the notification priority to `10`; 57 | /// otherwise, use `5`. 58 | /// 59 | /// The location push type is recommended for iOS and iPadOS. It isn’t available on macOS, tvOS, and watchOS. 60 | /// 61 | /// - Important: If you set this push type, the topic must use your app’s bundle ID with `.location-query` appended to the end. 62 | /// 63 | /// - Important: The location push type supports only token-based authentication. 64 | public static let location = Self(configuration: .location) 65 | 66 | /// Use the voip push type for notifications that provide information about an incoming Voice-over-IP (VoIP) call. 67 | /// 68 | /// The voip push type is not available on watchOS. It is recommended on macOS, iOS, tvOS, and iPadOS. 69 | /// 70 | /// - Important: If you set this push type, the topic must use your app’s bundle ID with `.voip` appended to the end. 71 | /// 72 | /// - Important: If you’re using certificate-based authentication, you must also register the certificate for VoIP services. 73 | /// The topic is then part of the 1.2.840.113635.100.6.3.4 or 1.2.840.113635.100.6.3.6 extension. 74 | public static let voip = Self(configuration: .voip) 75 | 76 | /// Use the complication push type for notifications that contain update information for a watchOS app’s complications. 77 | /// 78 | /// The complication push type is recommended for watchOS and iOS. It is not available on macOS, tvOS, and iPadOS. 79 | /// 80 | /// - Important: If you set this push type, the topic must use your app’s bundle ID with `.complication` appended to the end. 81 | /// 82 | /// - Important: If you’re using certificate-based authentication, you must also register the certificate for WatchKit services. 83 | /// The topic is then part of the 1.2.840.113635.100.6.3.6 extension. 84 | public static let complication = Self(configuration: .complication) 85 | 86 | /// Use the fileprovider push type to signal changes to a File Provider extension. 87 | /// 88 | /// The fileprovider push type is not available on watchOS. It is recommended on macOS, iOS, tvOS, and iPadOS. 89 | /// 90 | /// - Important: If you set this push type, the topic must use your app’s bundle ID with `.pushkit.fileprovider` appended to the end. 91 | public static let fileprovider = Self(configuration: .fileprovider) 92 | 93 | /// Use the mdm push type for notifications that tell managed devices to contact the MDM server. 94 | /// 95 | /// The mdm push type is not available on watchOS. It is recommended on macOS, iOS, tvOS, and iPadOS. 96 | /// 97 | /// - Important: If you set this push type, you must use the topic from the UID attribute in the subject of your MDM push certificate. 98 | public static let mdm = Self(configuration: .mdm) 99 | 100 | /// Use the live activity push type to update your live activity. 101 | /// 102 | public static let liveactivity = Self(configuration: .liveactivity) 103 | 104 | /// Use the pushtotalk push type for notifications that provide information about an incoming Push to Talk (Ptt). 105 | /// 106 | /// Push to Talk services aren’t available to compatible iPad and iPhone apps running in visionOS. 107 | /// 108 | /// - Important: If you set this push type, the topic must use your app’s bundle ID with `.voip-ptt` appended to the end. 109 | /// 110 | public static let pushtotalk = Self(configuration: .pushtotalk) 111 | } 112 | -------------------------------------------------------------------------------- /Sources/APNSCore/APNSRequest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.UUID 16 | 17 | public struct APNSRequest { 18 | fileprivate final class _Storage { 19 | var message: Message 20 | var deviceToken: String 21 | var pushType: APNSPushType 22 | var expiration: APNSNotificationExpiration? 23 | var priority: APNSPriority? 24 | var apnsID: UUID? 25 | var topic: String? 26 | var collapseID: String? 27 | 28 | init( 29 | message: Message, 30 | deviceToken: String, 31 | pushType: APNSPushType, 32 | expiration: APNSNotificationExpiration?, 33 | priority: APNSPriority?, 34 | apnsID: UUID?, 35 | topic: String?, 36 | collapseID: String? 37 | ) { 38 | self.message = message 39 | self.deviceToken = deviceToken 40 | self.pushType = pushType 41 | self.expiration = expiration 42 | self.priority = priority 43 | self.apnsID = apnsID 44 | self.topic = topic 45 | self.collapseID = collapseID 46 | } 47 | } 48 | 49 | private var _storage: _Storage 50 | 51 | public var headers: [String: String] { 52 | var computedHeaders: [String: String] = [:] 53 | 54 | /// Push type 55 | computedHeaders["apns-push-type"] = pushType.configuration.rawValue 56 | 57 | /// APNS ID 58 | if let apnsID = apnsID { 59 | computedHeaders["apns-id"] = apnsID.uuidString.lowercased() 60 | } 61 | 62 | /// Expiration 63 | if let expiration = expiration?.expiration { 64 | computedHeaders["apns-expiration"] = "\(expiration)" 65 | } 66 | 67 | /// Priority 68 | if let priority = priority?.rawValue { 69 | computedHeaders["apns-priority"] = "\(priority)" 70 | } 71 | 72 | /// Topic 73 | if let topic = topic { 74 | computedHeaders["apns-topic"] = topic 75 | } 76 | 77 | /// Collapse ID 78 | if let collapseID = collapseID { 79 | computedHeaders["apns-collapse-id"] = collapseID 80 | } 81 | 82 | return computedHeaders 83 | } 84 | public init( 85 | message: Message, 86 | deviceToken: String, 87 | pushType: APNSPushType, 88 | expiration: APNSNotificationExpiration?, 89 | priority: APNSPriority?, 90 | apnsID: UUID?, 91 | topic: String?, 92 | collapseID: String? 93 | ) { 94 | self._storage = _Storage( 95 | message: message, 96 | deviceToken: deviceToken, 97 | pushType: pushType, 98 | expiration: expiration, 99 | priority: priority, 100 | apnsID: apnsID, 101 | topic: topic, 102 | collapseID: collapseID 103 | ) 104 | } 105 | } 106 | 107 | extension APNSRequest._Storage { 108 | func copy() -> APNSRequest._Storage { 109 | APNSRequest._Storage( 110 | message: message, 111 | deviceToken: deviceToken, 112 | pushType: pushType, 113 | expiration: expiration, 114 | priority: priority, 115 | apnsID: apnsID, 116 | topic: topic, 117 | collapseID: collapseID 118 | ) 119 | } 120 | } 121 | 122 | extension APNSRequest { 123 | 124 | public var message: Message { 125 | get { 126 | return self._storage.message 127 | } 128 | set { 129 | if !isKnownUniquelyReferenced(&self._storage) { 130 | self._storage = self._storage.copy() 131 | } 132 | self._storage.message = newValue 133 | } 134 | } 135 | 136 | public var deviceToken: String { 137 | get { 138 | return self._storage.deviceToken 139 | } 140 | set { 141 | if !isKnownUniquelyReferenced(&self._storage) { 142 | self._storage = self._storage.copy() 143 | } 144 | self._storage.deviceToken = newValue 145 | } 146 | } 147 | 148 | public var pushType: APNSPushType { 149 | get { 150 | return self._storage.pushType 151 | } 152 | set { 153 | if !isKnownUniquelyReferenced(&self._storage) { 154 | self._storage = self._storage.copy() 155 | } 156 | self._storage.pushType = newValue 157 | } 158 | } 159 | 160 | public var expiration: APNSNotificationExpiration? { 161 | get { 162 | return self._storage.expiration 163 | } 164 | set { 165 | if !isKnownUniquelyReferenced(&self._storage) { 166 | self._storage = self._storage.copy() 167 | } 168 | self._storage.expiration = newValue 169 | } 170 | } 171 | 172 | public var priority: APNSPriority? { 173 | get { 174 | return self._storage.priority 175 | } 176 | set { 177 | if !isKnownUniquelyReferenced(&self._storage) { 178 | self._storage = self._storage.copy() 179 | } 180 | self._storage.priority = newValue 181 | } 182 | } 183 | 184 | public var apnsID: UUID? { 185 | get { 186 | return self._storage.apnsID 187 | } 188 | set { 189 | if !isKnownUniquelyReferenced(&self._storage) { 190 | self._storage = self._storage.copy() 191 | } 192 | self._storage.apnsID = newValue 193 | } 194 | } 195 | 196 | public var topic: String? { 197 | get { 198 | return self._storage.topic 199 | } 200 | set { 201 | if !isKnownUniquelyReferenced(&self._storage) { 202 | self._storage = self._storage.copy() 203 | } 204 | self._storage.topic = newValue 205 | } 206 | } 207 | 208 | public var collapseID: String? { 209 | get { 210 | return self._storage.collapseID 211 | } 212 | set { 213 | if !isKnownUniquelyReferenced(&self._storage) { 214 | self._storage = self._storage.copy() 215 | } 216 | self._storage.collapseID = newValue 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Sources/APNSCore/APNSResponse.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.UUID 16 | 17 | /// The response of a successful APNs request. 18 | public struct APNSResponse: Hashable, Sendable { 19 | /// The same value as the `apnsID` send in the request. 20 | /// 21 | /// Use this value to identify the notification. If you don’t specify an `apnsID` in your request, 22 | /// APNs creates a new `UUID` and returns it in this header. 23 | public var apnsID: UUID? 24 | 25 | /// A unique ID for the notification used for development, as determined by the APNs servers. 26 | /// 27 | /// In the development or sandbox environement, this value can be used to look up information about notifications on the [Push Notifications Console](https://icloud.developer.apple.com/dashboard/notifications). This value is not provided in the production environement. 28 | public var apnsUniqueID: UUID? 29 | 30 | /// Initializes a new ``APNSResponse``. 31 | /// 32 | /// - Parameter apnsID: The same value as the `apnsID` send in the request. 33 | /// - Parameter apnsUniqueID: A unique ID for the notification used only in the development environment. 34 | public init(apnsID: UUID? = nil, apnsUniqueID: UUID? = nil) { 35 | self.apnsID = apnsID 36 | self.apnsUniqueID = apnsUniqueID 37 | } 38 | } 39 | 40 | /// The [Push Notifications Console](https://icloud.developer.apple.com/dashboard/notifications) expects IDs to be lowercased, so prep them ahead of time here to make it easier for users to copy and paste these IDs. 41 | extension APNSResponse: CustomStringConvertible { 42 | public var description: String { 43 | "APNSResponse(apns-id: \(apnsID?.uuidString.lowercased() ?? "nil"), apns-unique-id: \(apnsUniqueID?.uuidString.lowercased() ?? "nil"))" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/APNSCore/Alert/APNSAlertNotificationAPSStorage.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | struct APNSAlertNotificationAPSStorage: Encodable, Sendable { 16 | enum CodingKeys: String, CodingKey { 17 | case alert 18 | case badge 19 | case sound 20 | case threadID = "thread-id" 21 | case category 22 | case mutableContent = "mutable-content" 23 | case targetContentID = "target-content-id" 24 | case interruptionLevel = "interruption-level" 25 | case relevanceScore = "relevance-score" 26 | } 27 | 28 | var alert: APNSAlertNotificationContent 29 | 30 | var badge: Int? 31 | 32 | var sound: APNSAlertNotificationSound? 33 | 34 | var threadID: String? 35 | 36 | var category: String? 37 | 38 | var mutableContent: Double? 39 | 40 | var targetContentID: String? 41 | 42 | var interruptionLevel: APNSAlertNotificationInterruptionLevel? 43 | 44 | var relevanceScore: Double? { 45 | willSet { 46 | if let newValue = newValue { 47 | precondition(newValue >= 0 && newValue <= 1, "The relevance score can only be between 0 and 1") 48 | } 49 | } 50 | } 51 | 52 | init( 53 | alert: APNSAlertNotificationContent, 54 | badge: Int? = nil, 55 | sound: APNSAlertNotificationSound? = nil, 56 | threadID: String? = nil, 57 | category: String? = nil, 58 | mutableContent: Double? = nil, 59 | targetContentID: String? = nil, 60 | interruptionLevel: APNSAlertNotificationInterruptionLevel? = nil, 61 | relevanceScore: Double? = nil 62 | ) { 63 | if let relevanceScore = relevanceScore { 64 | precondition(relevanceScore >= 0 && relevanceScore <= 1, "The relevance score can only be between 0 and 1") 65 | } 66 | self.alert = alert 67 | self.badge = badge 68 | self.sound = sound 69 | self.threadID = threadID 70 | self.category = category 71 | self.mutableContent = mutableContent 72 | self.targetContentID = targetContentID 73 | self.interruptionLevel = interruptionLevel 74 | self.relevanceScore = relevanceScore 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/APNSCore/Alert/APNSAlertNotificationContent.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// The information for displaying an alert. 16 | public struct APNSAlertNotificationContent: Encodable, Sendable { 17 | public struct StringValue: Encodable, Hashable, Sendable { 18 | internal enum Configuration: Encodable, Hashable { 19 | case raw(String) 20 | case localized(key: String, arguments: [String]) 21 | } 22 | 23 | internal var configuration: Configuration 24 | 25 | /// Sends the raw value to APNs. 26 | /// 27 | /// Use this if you are localizing your values from the backend. 28 | /// 29 | /// - Parameters: 30 | /// - value: The raw string. 31 | public static func raw(_ value: String) -> Self { 32 | Self(configuration: .raw(value)) 33 | } 34 | 35 | /// Sends a localization key and arguments. 36 | /// 37 | /// - Parameters: 38 | /// - key: The key that will be retrieved from your app's `Localizable.strings`. The key *must* contain the name of a key in your strings file. 39 | /// - arugments: An array of strings containing replacement values for variables in your `key` string. 40 | /// Each %@ character in the string specified by the `key` is replaced by a value from this array. 41 | /// The first item in the array replaces the first instance of the %@ character in the string, the second item replaces the second instance, and so on. 42 | public static func localized(key: String, arguments: [String]) -> Self { 43 | Self(configuration: .localized(key: key, arguments: arguments)) 44 | } 45 | 46 | public func encode(to encoder: Encoder) throws { 47 | var container = encoder.singleValueContainer() 48 | try container.encode(self.configuration) 49 | } 50 | } 51 | 52 | enum CodingKeys: String, CodingKey { 53 | case title 54 | case subtitle 55 | case body 56 | case launchImage = "launch-image" 57 | case titleLocalizationKey = "title-loc-key" 58 | case titleLocalizationArguments = "title-loc-args" 59 | case subtitleLocalizationKey = "subtitle-loc-key" 60 | case subtitleLocalizationArguments = "subtitle-loc-args" 61 | case bodyLocalizationKey = "loc-key" 62 | case bodyLocalizationArguments = "loc-args" 63 | } 64 | 65 | /// The title of the notification. Apple Watch displays this string in the short look notification interface. 66 | /// Specify a string that’s quickly understood by the user. 67 | public var title: StringValue? 68 | 69 | /// Additional information that explains the purpose of the notification. 70 | public var subtitle: StringValue? 71 | 72 | /// The content of the alert message. 73 | public var body: StringValue? 74 | 75 | /// The name of the launch image file to display. If the user chooses to launch your app, 76 | /// the contents of the specified image or storyboard file are displayed instead of your app’s normal launch image. 77 | public var launchImage: String? 78 | 79 | /// Initializes a new ``APNSAlertNotificationContent``. 80 | /// 81 | /// - Parameters: 82 | /// - title: The title of the notification. 83 | /// - subtitle: Additional information that explains the purpose of the notification. 84 | /// - body: The content of the alert message. 85 | /// - launchImage: The name of the launch image file to display. 86 | public init( 87 | title: APNSAlertNotificationContent.StringValue? = nil, 88 | subtitle: APNSAlertNotificationContent.StringValue? = nil, 89 | body: APNSAlertNotificationContent.StringValue? = nil, 90 | launchImage: String? = nil 91 | ) { 92 | self.title = title 93 | self.subtitle = subtitle 94 | self.body = body 95 | self.launchImage = launchImage 96 | } 97 | 98 | public func encode(to encoder: Encoder) throws { 99 | var container = encoder.container(keyedBy: CodingKeys.self) 100 | 101 | try self.encode( 102 | value: self.title, 103 | into: &container, 104 | rawKey: .title, 105 | localizedKey: .titleLocalizationKey, 106 | localizedArgumentsKey: .titleLocalizationArguments 107 | ) 108 | try self.encode( 109 | value: self.subtitle, 110 | into: &container, 111 | rawKey: .subtitle, 112 | localizedKey: .subtitleLocalizationKey, 113 | localizedArgumentsKey: .subtitleLocalizationArguments 114 | ) 115 | try self.encode( 116 | value: self.body, 117 | into: &container, 118 | rawKey: .body, 119 | localizedKey: .bodyLocalizationKey, 120 | localizedArgumentsKey: .bodyLocalizationArguments 121 | ) 122 | try container.encodeIfPresent(self.launchImage, forKey: .launchImage) 123 | } 124 | 125 | private func encode( 126 | value: StringValue?, 127 | into container: inout KeyedEncodingContainer, 128 | rawKey: KeyedEncodingContainer.Key, 129 | localizedKey: KeyedEncodingContainer.Key, 130 | localizedArgumentsKey: KeyedEncodingContainer.Key 131 | ) throws { 132 | switch value?.configuration { 133 | case .raw(let value): 134 | try container.encode(value, forKey: rawKey) 135 | case .localized(let key, let arguments): 136 | try container.encode(key, forKey: localizedKey) 137 | try container.encode(arguments, forKey: localizedArgumentsKey) 138 | case .none: 139 | break 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/APNSCore/Alert/APNSAlertNotificationInterruptionLevel.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// A struct to indicate the importance and delivery timing of a notification 16 | public struct APNSAlertNotificationInterruptionLevel: Encodable, Hashable, Sendable { 17 | internal var rawValue: String 18 | 19 | /// The system adds the notification to the notification list without lighting up the screen or playing a sound. 20 | public static let passive = Self(rawValue: "passive") 21 | 22 | /// The system presents the notification immediately, lights up the screen, and can play a sound. 23 | public static let active = Self(rawValue: "active") 24 | 25 | /// The system presents the notification immediately, lights up the screen, and can play a sound, 26 | /// but won’t break through system notification controls. 27 | public static let timeSensitive = Self(rawValue: "time-sensitive") 28 | 29 | /// The system presents the notification immediately, lights up the screen, and bypasses the mute switch to play a sound. 30 | public static let critical = Self(rawValue: "critical") 31 | 32 | public func encode(to encoder: Encoder) throws { 33 | var container = encoder.singleValueContainer() 34 | try container.encode(self.rawValue) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/APNSCore/Alert/APNSAlertNotificationSound.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// A sound to play for an alert notification. 16 | public struct APNSAlertNotificationSound: Encodable, Hashable, Sendable { 17 | internal enum Configuration: Hashable { 18 | case systemSound 19 | case fileName(String) 20 | case critical(fileName: String, volume: Double) 21 | } 22 | 23 | internal enum CriticalCodingKeys: String, CodingKey { 24 | case critical 25 | case name 26 | case volume 27 | } 28 | 29 | internal var configuration: Configuration 30 | 31 | /// Plays the default system sound. 32 | public static let `default` = Self(configuration: .systemSound) 33 | 34 | /// Plays a sound file with the given name in your app's main bundle or 35 | /// in the `Library/Sounds` folder of your app's container directory. 36 | /// 37 | /// - Important: For `critical` alerts use the ``APNSAlertNotificationSound.critical`` method instead. 38 | /// 39 | /// - Parameters: 40 | /// - fileName: The file name of the sound file. 41 | public static func fileName(_ fileName: String) -> Self { 42 | Self(configuration: .fileName(fileName)) 43 | } 44 | 45 | /// Plays a sound file with the given name for critical alerts. The file needs to be in your app's main bundle or 46 | /// in the `Library/Sounds` folder of your app's container directory. 47 | /// 48 | /// - Parameters: 49 | /// - fileName: The file name of the sound file. 50 | /// - volume: The volume for the critical alert’s sound. Set this to a value between 0 (silent) and 1 (full volume). 51 | public static func critical(fileName: String, volume: Double) -> Self { 52 | precondition(volume >= 0 && volume <= 1, "The volume can only be between 0 and 1") 53 | 54 | return Self(configuration: .critical(fileName: fileName, volume: volume)) 55 | } 56 | 57 | public func encode(to encoder: Encoder) throws { 58 | switch self.configuration { 59 | case .systemSound: 60 | var container = encoder.singleValueContainer() 61 | try container.encode("default") 62 | case .fileName(let fileName): 63 | var container = encoder.singleValueContainer() 64 | try container.encode(fileName) 65 | case .critical(let fileName, let volume): 66 | var container = encoder.container(keyedBy: CriticalCodingKeys.self) 67 | try container.encode(1, forKey: .critical) 68 | try container.encode(fileName, forKey: .name) 69 | try container.encode(volume, forKey: .volume) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/APNSCore/Alert/APNSClient+Alert.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | 16 | extension APNSClientProtocol { 17 | /// Sends an alert notification to APNs. 18 | /// 19 | /// - Parameters: 20 | /// - notification: The notification to send. 21 | /// 22 | /// - deviceToken: The hexadecimal bytes that identify the user’s device. Your app receives the bytes for this device token 23 | /// when registering for remote notifications. 24 | /// 25 | /// 26 | /// - logger: The logger to use for sending this notification. 27 | @discardableResult 28 | @inlinable 29 | public func sendAlertNotification( 30 | _ notification: APNSAlertNotification, 31 | deviceToken: String 32 | ) async throws -> APNSResponse { 33 | let request = APNSRequest( 34 | message: notification, 35 | deviceToken: deviceToken, 36 | pushType: .alert, 37 | expiration: notification.expiration, 38 | priority: notification.priority, 39 | apnsID: notification.apnsID, 40 | topic: notification.topic, 41 | collapseID: notification.collapseID 42 | ) 43 | return try await send(request) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/APNSCore/Background/APNSBackgroundNotification.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.UUID 16 | 17 | /// A background notification. 18 | /// 19 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 20 | /// It is **important** that you do not encode anything with the key `aps`. 21 | public struct APNSBackgroundNotification: APNSMessage { 22 | @usableFromInline 23 | struct APS: Encodable, Sendable { 24 | enum CodingKeys: String, CodingKey { 25 | case contentAvailable = "content-available" 26 | } 27 | 28 | let contentAvailable: Int = 1 29 | } 30 | 31 | @usableFromInline 32 | enum CodingKeys: CodingKey { 33 | case aps 34 | } 35 | 36 | /// The fixed content to indicate that this is a background notification. 37 | @usableFromInline 38 | /* private */ internal let aps = APS() 39 | 40 | /// A canonical UUID that identifies the notification. If there is an error sending the notification, 41 | /// APNs uses this value to identify the notification to your server. The canonical form is 32 lowercase hexadecimal digits, 42 | /// displayed in five groups separated by hyphens in the form 8-4-4-4-12. An example UUID is as follows: 43 | /// `123e4567-e89b-12d3-a456-42665544000`. 44 | /// 45 | /// If you omit this, a new UUID is created by APNs and returned in the response. 46 | public var apnsID: UUID? 47 | 48 | /// The date when the notification is no longer valid and can be discarded. If this value is not `none`, 49 | /// APNs stores the notification and tries to deliver it at least once, 50 | /// repeating the attempt as needed if it is unable to deliver the notification the first time. 51 | /// If the value is `immediately`, APNs treats the notification as if it expires immediately 52 | /// and does not store the notification or attempt to redeliver it. 53 | public var expiration: APNSNotificationExpiration 54 | 55 | /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID. 56 | public var topic: String 57 | 58 | /// Your custom payload. 59 | public var payload: Payload 60 | 61 | /// Initializes a new ``APNSBackgroundNotification``. 62 | /// 63 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 64 | /// It is **important** that you do not encode anything with the key `aps` 65 | /// 66 | /// - Parameters: 67 | /// - payload: Your custom payload. 68 | /// 69 | /// - expiration: The date when the notification is no longer valid and can be discarded. 70 | /// 71 | /// - topic: The topic for the notification. In general, the topic is your app’s bundle ID/app ID. 72 | /// 73 | /// - apnsID: A canonical UUID that identifies the notification. 74 | @inlinable 75 | public init( 76 | expiration: APNSNotificationExpiration, 77 | topic: String, 78 | payload: Payload, 79 | apnsID: UUID? = nil 80 | ) { 81 | self.payload = payload 82 | self.apnsID = apnsID 83 | self.expiration = expiration 84 | self.topic = topic 85 | } 86 | 87 | @inlinable 88 | public func encode(to encoder: Encoder) throws { 89 | // First we encode the user payload since this might use the `aps` key 90 | // and we override it afterward. 91 | try self.payload.encode(to: encoder) 92 | var container = encoder.container(keyedBy: CodingKeys.self) 93 | try container.encode(self.aps, forKey: .aps) 94 | } 95 | } 96 | 97 | extension APNSBackgroundNotification where Payload == EmptyPayload { 98 | public init( 99 | expiration: APNSNotificationExpiration, 100 | topic: String, 101 | apnsID: UUID? = nil 102 | ) { 103 | self.init( 104 | expiration: expiration, 105 | topic: topic, 106 | payload: EmptyPayload(), 107 | apnsID: apnsID 108 | ) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/APNSCore/Background/APNSClient+Background.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | 16 | extension APNSClientProtocol { 17 | /// Sends a background update notification to APNs. 18 | /// 19 | /// - Parameters: 20 | /// - notification: The notification to send. 21 | /// 22 | /// - deviceToken: The hexadecimal bytes that identify the user’s device. Your app receives the bytes for this device token 23 | /// when registering for remote notifications. 24 | /// 25 | /// 26 | /// - logger: The logger to use for sending this notification. 27 | @discardableResult 28 | @inlinable 29 | public func sendBackgroundNotification( 30 | _ notification: APNSBackgroundNotification, 31 | deviceToken: String 32 | ) async throws -> APNSResponse { 33 | let request = APNSRequest( 34 | message: notification, 35 | deviceToken: deviceToken, 36 | pushType: .background, 37 | expiration: notification.expiration, 38 | priority: .consideringDevicePower, 39 | apnsID: notification.apnsID, 40 | topic: notification.topic, 41 | collapseID: nil 42 | ) 43 | return try await send(request) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/APNSCore/Complication/APNSClient+Complication.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | extension APNSClientProtocol { 16 | /// Sends a complication notification to APNs. 17 | /// 18 | /// - Parameters: 19 | /// - notification: The notification to send. 20 | /// 21 | /// - deviceToken: The hexadecimal bytes that identify the user’s device. Your app receives the bytes for this device token 22 | /// when registering for remote notifications. 23 | /// 24 | /// 25 | /// - logger: The logger to use for sending this notification. 26 | @discardableResult 27 | @inlinable 28 | public func sendComplicationNotification( 29 | _ notification: APNSComplicationNotification, 30 | deviceToken: String 31 | ) async throws -> APNSResponse { 32 | let request = APNSRequest( 33 | message: notification, 34 | deviceToken: deviceToken, 35 | pushType: .complication, 36 | expiration: notification.expiration, 37 | priority: notification.priority, 38 | apnsID: notification.apnsID, 39 | topic: notification.topic, 40 | collapseID: nil 41 | ) 42 | return try await send(request) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/APNSCore/Complication/APNSComplicationNotification.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.UUID 16 | 17 | /// A complication notification. 18 | public struct APNSComplicationNotification: APNSMessage { 19 | /// A canonical UUID that identifies the notification. If there is an error sending the notification, 20 | /// APNs uses this value to identify the notification to your server. The canonical form is 32 lowercase hexadecimal digits, 21 | /// displayed in five groups separated by hyphens in the form 8-4-4-4-12. An example UUID is as follows: 22 | /// `123e4567-e89b-12d3-a456-42665544000`. 23 | /// 24 | /// If you omit this, a new UUID is created by APNs and returned in the response. 25 | public var apnsID: UUID? 26 | 27 | /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.complication`. 28 | public var topic: String 29 | 30 | /// The date when the notification is no longer valid and can be discarded. If this value is not `none`, 31 | /// APNs stores the notification and tries to deliver it at least once, 32 | /// repeating the attempt as needed if it is unable to deliver the notification the first time. 33 | /// If the value is `immediately`, APNs treats the notification as if it expires immediately 34 | /// and does not store the notification or attempt to redeliver it. 35 | public var expiration: APNSNotificationExpiration 36 | 37 | /// The priority of the notification. 38 | public var priority: APNSPriority 39 | 40 | /// Your custom payload. 41 | public var payload: Payload 42 | 43 | /// Initializes a new ``APNSComplicationNotification``. 44 | /// 45 | /// - Parameters: 46 | /// - expiration: The date when the notification is no longer valid and can be discarded. 47 | /// - priority: The priority of the notification. 48 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.complication`. 49 | /// - payload: Your custom payload. 50 | /// - apnsID: A canonical UUID that identifies the notification. 51 | @inlinable 52 | public init( 53 | expiration: APNSNotificationExpiration, 54 | priority: APNSPriority, 55 | appID: String, 56 | payload: Payload, 57 | apnsID: UUID? = nil 58 | ) { 59 | self.init( 60 | expiration: expiration, 61 | priority: priority, 62 | topic: appID + ".complication", 63 | payload: payload, 64 | apnsID: apnsID 65 | ) 66 | } 67 | 68 | /// Initializes a new ``APNSVoIPNotification``. 69 | /// 70 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 71 | /// It is **important** that you do not encode anything with the key `aps` 72 | /// 73 | /// - Parameters: 74 | /// - expiration: The date when the notification is no longer valid and can be discarded. 75 | /// - priority: The priority of the notification. 76 | /// - topic: The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.complication`. 77 | /// - payload: Your custom payload. 78 | /// - apnsID: A canonical UUID that identifies the notification. 79 | @inlinable 80 | public init( 81 | expiration: APNSNotificationExpiration, 82 | priority: APNSPriority, 83 | topic: String, 84 | payload: Payload, 85 | apnsID: UUID? = nil 86 | ) { 87 | self.expiration = expiration 88 | self.priority = priority 89 | self.topic = topic 90 | self.payload = payload 91 | self.apnsID = apnsID 92 | } 93 | } 94 | 95 | extension APNSComplicationNotification where Payload == EmptyPayload { 96 | /// Initializes a new ``APNSComplicationNotification`` with an EmptyPayload. 97 | /// 98 | /// - Parameters: 99 | /// - expiration: The date when the notification is no longer valid and can be discarded. 100 | /// - priority: The priority of the notification. 101 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.complication`. 102 | /// - payload: Your custom payload. 103 | /// - apnsID: A canonical UUID that identifies the notification. 104 | public init( 105 | expiration: APNSNotificationExpiration, 106 | priority: APNSPriority, 107 | appID: String, 108 | apnsID: UUID? = nil 109 | ) { 110 | self.init( 111 | expiration: expiration, 112 | priority: priority, 113 | topic: appID + ".complication", 114 | payload: EmptyPayload(), 115 | apnsID: apnsID 116 | ) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/APNSCore/EmptyPayload.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-extras-base64 open source project 4 | // 5 | // Copyright (c) 2022 the swift-extras-base64 project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | public struct EmptyPayload: Encodable, Sendable { 15 | public init() {} 16 | } 17 | -------------------------------------------------------------------------------- /Sources/APNSCore/FileProvider/APNSClient+FileProvider.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | extension APNSClientProtocol { 16 | /// Sends a file provider notification to APNs. 17 | /// 18 | /// - Parameters: 19 | /// - notification: The notification to send. 20 | /// 21 | /// - deviceToken: The hexadecimal bytes that identify the user’s device. Your app receives the bytes for this device token 22 | /// when registering for remote notifications. 23 | /// 24 | /// 25 | /// - logger: The logger to use for sending this notification. 26 | @discardableResult 27 | @inlinable 28 | public func sendFileProviderNotification( 29 | _ notification: APNSFileProviderNotification, 30 | deviceToken: String 31 | ) async throws -> APNSResponse { 32 | let request = APNSRequest( 33 | message: notification, 34 | deviceToken: deviceToken, 35 | pushType: .fileprovider, 36 | expiration: notification.expiration, 37 | // This always needs to be consideringDevicePower otherwise APNs returns an error 38 | priority: .consideringDevicePower, 39 | apnsID: notification.apnsID, 40 | topic: notification.topic, 41 | collapseID: nil 42 | ) 43 | return try await send(request) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/APNSCore/FileProvider/APNSFileProviderNotification.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.UUID 16 | 17 | /// A file provider notification. 18 | public struct APNSFileProviderNotification: APNSMessage { 19 | /// A canonical UUID that identifies the notification. If there is an error sending the notification, 20 | /// APNs uses this value to identify the notification to your server. The canonical form is 32 lowercase hexadecimal digits, 21 | /// displayed in five groups separated by hyphens in the form 8-4-4-4-12. An example UUID is as follows: 22 | /// `123e4567-e89b-12d3-a456-42665544000`. 23 | /// 24 | /// If you omit this, a new UUID is created by APNs and returned in the response. 25 | public var apnsID: UUID? 26 | 27 | /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.voip`. 28 | public var topic: String 29 | 30 | /// The date when the notification is no longer valid and can be discarded. If this value is not `none`, 31 | /// APNs stores the notification and tries to deliver it at least once, 32 | /// repeating the attempt as needed if it is unable to deliver the notification the first time. 33 | /// If the value is `immediately`, APNs treats the notification as if it expires immediately 34 | /// and does not store the notification or attempt to redeliver it. 35 | public var expiration: APNSNotificationExpiration 36 | 37 | /// Your custom payload. 38 | public var payload: Payload 39 | 40 | /// Initializes a new ``APNSFileProviderNotification``. 41 | /// 42 | /// - Parameters: 43 | /// - expiration: The date when the notification is no longer valid and can be discarded 44 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.pushkit.fileprovider`. 45 | /// - payload: Your custom payload. 46 | /// - apnsID: A canonical UUID that identifies the notification. 47 | @inlinable 48 | public init( 49 | expiration: APNSNotificationExpiration, 50 | appID: String, 51 | payload: Payload, 52 | apnsID: UUID? = nil 53 | ) { 54 | self.init( 55 | expiration: expiration, 56 | topic: appID + ".pushkit.fileprovider", 57 | payload: payload, 58 | apnsID: apnsID 59 | ) 60 | } 61 | 62 | /// Initializes a new ``APNSFileProviderNotification``. 63 | /// 64 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 65 | /// It is **important** that you do not encode anything with the key `aps` 66 | /// 67 | /// - Parameters: 68 | /// - expiration: The date when the notification is no longer valid and can be discarded 69 | /// - topic: The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.pushkit.fileprovider`. 70 | /// - payload: Your custom payload. 71 | /// - apnsID: A canonical UUID that identifies the notification. 72 | @inlinable 73 | public init( 74 | expiration: APNSNotificationExpiration, 75 | topic: String, 76 | payload: Payload, 77 | apnsID: UUID? = nil 78 | ) { 79 | self.expiration = expiration 80 | self.topic = topic 81 | self.payload = payload 82 | self.apnsID = apnsID 83 | } 84 | } 85 | 86 | extension APNSFileProviderNotification where Payload == EmptyPayload { 87 | /// Initializes a new ``APNSFileProviderNotification`` with an EmptyPayload. 88 | /// 89 | /// - Parameters: 90 | /// - expiration: The date when the notification is no longer valid and can be discarded 91 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.pushkit.fileprovider`. 92 | /// - payload: Your custom payload. 93 | /// - apnsID: A canonical UUID that identifies the notification. 94 | public init( 95 | expiration: APNSNotificationExpiration, 96 | appID: String, 97 | apnsID: UUID? = nil 98 | ) { 99 | self.init( 100 | expiration: expiration, 101 | topic: appID + ".pushkit.fileprovider", 102 | payload: EmptyPayload(), 103 | apnsID: apnsID 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/APNSCore/LiveActivity/APNSClient+LiveActivity.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | extension APNSClientProtocol { 16 | /// Sends a live activity notification. 17 | /// 18 | /// - Parameters: 19 | /// - notification: The notification to send. 20 | /// 21 | /// - deviceToken: The hexadecimal bytes use to send live activity notification. Your app receives the bytes for this activity token 22 | /// from `pushTokenUpdates` async property of a live activity. 23 | /// 24 | /// 25 | /// - logger: The logger to use for sending this notification. 26 | @discardableResult 27 | @inlinable 28 | public func sendLiveActivityNotification( 29 | _ notification: APNSLiveActivityNotification, 30 | deviceToken: String 31 | ) async throws -> APNSResponse { 32 | let request = APNSRequest( 33 | message: notification, 34 | deviceToken: deviceToken, 35 | pushType: .liveactivity, 36 | expiration: notification.expiration, 37 | priority: notification.priority, 38 | apnsID: notification.apnsID, 39 | topic: notification.topic, 40 | collapseID: nil 41 | ) 42 | return try await send(request) 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Sources/APNSCore/LiveActivity/APNSLiveActivityDismissalDate.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Date 16 | 17 | public struct APNSLiveActivityDismissalDate: Hashable, Sendable { 18 | /// The date at which the live activity will be dismissed 19 | /// This value is a UNIX epoch expressed in seconds (UTC) 20 | @usableFromInline 21 | let dismissal: Int? 22 | 23 | /// Omits sending an dismissal date for APNs. APNs will default to a default value for dismissal time. 24 | public static let none = Self(dismissal: nil) 25 | 26 | /// Have live activity dismiss immediately when end received 27 | public static let immediately = Self(dismissal: 0) 28 | 29 | /// Specify dismissal as a unix time stamp, if in past will dismiss 30 | /// immedidately. 31 | public static func timeIntervalSince1970InSeconds(_ timeInterval: Int) -> Self { 32 | Self(dismissal: timeInterval) 33 | } 34 | 35 | /// Specify dismissal as a date, if in past will dismiss 36 | /// immedidately. 37 | public static func date(_ date: Date) -> Self { 38 | Self(dismissal: Int(date.timeIntervalSince1970)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/APNSCore/LiveActivity/APNSLiveActivityNotification.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.UUID 16 | 17 | /// A live activity notification. 18 | /// 19 | /// It is **important** that you do not encode anything with the key `aps`. 20 | public struct APNSLiveActivityNotification: APNSMessage { 21 | enum CodingKeys: CodingKey { 22 | case aps 23 | } 24 | 25 | /// The fixed content to indicate that this is a background notification. 26 | private var aps: APNSLiveActivityNotificationAPSStorage 27 | 28 | /// Timestamp when sending notification 29 | public var timestamp: Int { 30 | get { 31 | return self.aps.timestamp 32 | } 33 | 34 | set { 35 | self.aps.timestamp = newValue 36 | } 37 | } 38 | 39 | /// Event type e.g. update 40 | public var event: APNSLiveActivityNotificationEvent { 41 | get { 42 | return APNSLiveActivityNotificationEvent(rawValue: self.aps.event) 43 | } 44 | 45 | set { 46 | self.aps.event = newValue.rawValue 47 | } 48 | } 49 | 50 | /// The dynamic content of a Live Activity. 51 | public var contentState: ContentState { 52 | get { 53 | return self.aps.contentState 54 | } 55 | 56 | set { 57 | self.aps.contentState = newValue 58 | } 59 | } 60 | 61 | public var dismissalDate: APNSLiveActivityDismissalDate? { 62 | get { 63 | return .init(dismissal: self.aps.dismissalDate) 64 | } 65 | set { 66 | self.aps.dismissalDate = newValue?.dismissal 67 | } 68 | } 69 | 70 | /// A canonical UUID that identifies the notification. If there is an error sending the notification, 71 | /// APNs uses this value to identify the notification to your server. The canonical form is 32 lowercase hexadecimal digits, 72 | /// displayed in five groups separated by hyphens in the form 8-4-4-4-12. An example UUID is as follows: 73 | /// `123e4567-e89b-12d3-a456-42665544000`. 74 | /// 75 | /// If you omit this, a new UUID is created by APNs and returned in the response. 76 | public var apnsID: UUID? 77 | 78 | /// The date when the notification is no longer valid and can be discarded. If this value is not `none`, 79 | /// APNs stores the notification and tries to deliver it at least once, 80 | /// repeating the attempt as needed if it is unable to deliver the notification the first time. 81 | /// If the value is `immediately`, APNs treats the notification as if it expires immediately 82 | /// and does not store the notification or attempt to redeliver it. 83 | public var expiration: APNSNotificationExpiration 84 | 85 | /// The priority of the notification. 86 | public var priority: APNSPriority 87 | 88 | /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID. 89 | public var topic: String 90 | 91 | /// Initializes a new ``APNSLiveActivityNotification``. 92 | /// 93 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 94 | /// It is **important** that you do not encode anything with the key `aps` 95 | /// 96 | /// - Parameters: 97 | /// - expiration: The date when the notification is no longer valid and can be discarded. 98 | /// - priority: The priority of the notification. 99 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.push-type.liveactivity`. 100 | /// - apnsID: A canonical UUID that identifies the notification. 101 | /// - contentState: Updated content-state of live activity 102 | /// - event: event type e.g. update 103 | /// - alert: Optional alert content for the notification. 104 | /// - timestamp: Timestamp when sending notification 105 | /// - dismissalDate: Timestamp when to dismiss live notification when sent with `end`, if in the past 106 | /// dismiss immediately 107 | public init( 108 | expiration: APNSNotificationExpiration, 109 | priority: APNSPriority, 110 | appID: String, 111 | contentState: ContentState, 112 | event: APNSLiveActivityNotificationEvent, 113 | alert: APNSAlertNotificationContent? = nil, 114 | timestamp: Int, 115 | dismissalDate: APNSLiveActivityDismissalDate = .none, 116 | apnsID: UUID? = nil 117 | ) { 118 | self.init( 119 | expiration: expiration, 120 | priority: priority, 121 | topic: appID + ".push-type.liveactivity", 122 | contentState: contentState, 123 | event: event, 124 | alert: alert, 125 | timestamp: timestamp, 126 | dismissalDate: dismissalDate 127 | ) 128 | } 129 | 130 | /// Initializes a new ``APNSLiveActivityNotification``. 131 | /// 132 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 133 | /// It is **important** that you do not encode anything with the key `aps` 134 | /// 135 | /// - Parameters: 136 | /// - expiration: The date when the notification is no longer valid and can be discarded. 137 | /// - priority: The priority of the notification. 138 | /// - topic: The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.push-type.liveactivity`. 139 | /// - apnsID: A canonical UUID that identifies the notification. 140 | /// - contentState: Updated content-state of live activity 141 | /// - event: event type e.g. update 142 | /// - alert: Optional alert content for the notification. 143 | /// - timestamp: Timestamp when sending notification 144 | /// - dismissalDate: Timestamp when to dismiss live notification when sent with `end`, if in the past 145 | /// dismiss immediately 146 | public init( 147 | expiration: APNSNotificationExpiration, 148 | priority: APNSPriority, 149 | topic: String, 150 | apnsID: UUID? = nil, 151 | contentState: ContentState, 152 | event: APNSLiveActivityNotificationEvent, 153 | alert: APNSAlertNotificationContent? = nil, 154 | timestamp: Int, 155 | dismissalDate: APNSLiveActivityDismissalDate = .none 156 | ) { 157 | self.aps = APNSLiveActivityNotificationAPSStorage( 158 | timestamp: timestamp, 159 | event: event.rawValue, 160 | contentState: contentState, 161 | dismissalDate: dismissalDate.dismissal, 162 | alert: alert 163 | ) 164 | self.apnsID = apnsID 165 | self.expiration = expiration 166 | self.priority = priority 167 | self.topic = topic 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Sources/APNSCore/LiveActivity/APNSLiveActivityNotificationAPSStorage.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | struct APNSLiveActivityNotificationAPSStorage: Encodable { 16 | enum CodingKeys: String, CodingKey { 17 | case timestamp = "timestamp" 18 | case event = "event" 19 | case contentState = "content-state" 20 | case dismissalDate = "dismissal-date" 21 | case alert = "alert" 22 | } 23 | 24 | var timestamp: Int 25 | var event: String 26 | var contentState: ContentState 27 | var dismissalDate: Int? 28 | var alert: APNSAlertNotificationContent? 29 | 30 | init( 31 | timestamp: Int, 32 | event: String, 33 | contentState: ContentState, 34 | dismissalDate: Int?, 35 | alert: APNSAlertNotificationContent? = nil 36 | ) { 37 | self.timestamp = timestamp 38 | self.contentState = contentState 39 | self.dismissalDate = dismissalDate 40 | self.event = event 41 | self.alert = alert 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/APNSCore/LiveActivity/APNSLiveActivityNotificationEvent.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public struct APNSLiveActivityNotificationEvent: Hashable, Sendable { 16 | /// The underlying raw value that is send to APNs. 17 | @usableFromInline 18 | internal let rawValue: String 19 | 20 | /// Specifies that live activity should be started 21 | public static let start = Self(rawValue: "start") 22 | 23 | /// Specifies that live activity should be updated 24 | public static let update = Self(rawValue: "update") 25 | 26 | /// Specifies that live activity should be ended 27 | public static let end = Self(rawValue: "end") 28 | } 29 | -------------------------------------------------------------------------------- /Sources/APNSCore/LiveActivity/APNSStartLiveActivityNotification.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.UUID 16 | 17 | /// A notification that starts a live activity 18 | /// 19 | /// It is **important** that you do not encode anything with the key `aps`. 20 | public struct APNSStartLiveActivityNotification: 21 | APNSMessage 22 | { 23 | enum CodingKeys: CodingKey { 24 | case aps 25 | } 26 | 27 | /// The fixed content to indicate that this is a background notification. 28 | private var aps: APNSStartLiveActivityNotificationAPSStorage 29 | 30 | /// Timestamp when sending notification 31 | public var timestamp: Int { 32 | get { 33 | return self.aps.timestamp 34 | } 35 | 36 | set { 37 | self.aps.timestamp = newValue 38 | } 39 | } 40 | 41 | public var alert: APNSAlertNotificationContent { 42 | get { 43 | return self.aps.alert 44 | } 45 | 46 | set { 47 | self.aps.alert = newValue 48 | } 49 | } 50 | 51 | /// The dynamic content of a Live Activity. 52 | public var contentState: ContentState { 53 | get { 54 | return self.aps.contentState 55 | } 56 | 57 | set { 58 | self.aps.contentState = newValue 59 | } 60 | } 61 | 62 | public var dismissalDate: APNSLiveActivityDismissalDate? { 63 | get { 64 | return .init(dismissal: self.aps.dismissalDate) 65 | } 66 | set { 67 | self.aps.dismissalDate = newValue?.dismissal 68 | } 69 | } 70 | 71 | /// A canonical UUID that identifies the notification. If there is an error sending the notification, 72 | /// APNs uses this value to identify the notification to your server. The canonical form is 32 lowercase hexadecimal digits, 73 | /// displayed in five groups separated by hyphens in the form 8-4-4-4-12. An example UUID is as follows: 74 | /// `123e4567-e89b-12d3-a456-42665544000`. 75 | /// 76 | /// If you omit this, a new UUID is created by APNs and returned in the response. 77 | public var apnsID: UUID? 78 | 79 | /// The date when the notification is no longer valid and can be discarded. If this value is not `none`, 80 | /// APNs stores the notification and tries to deliver it at least once, 81 | /// repeating the attempt as needed if it is unable to deliver the notification the first time. 82 | /// If the value is `immediately`, APNs treats the notification as if it expires immediately 83 | /// and does not store the notification or attempt to redeliver it. 84 | public var expiration: APNSNotificationExpiration 85 | 86 | /// The priority of the notification. 87 | public var priority: APNSPriority 88 | 89 | /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID. 90 | public var topic: String 91 | 92 | /// Initializes a new ``APNSStartLiveActivityNotification``. 93 | /// 94 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 95 | /// It is **important** that you do not encode anything with the key `aps` 96 | /// 97 | /// - Parameters: 98 | /// - expiration: The date when the notification is no longer valid and can be discarded. 99 | /// - priority: The priority of the notification. 100 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.push-type.liveactivity`. 101 | /// - contentState: Updated content-state of live activity 102 | /// - timestamp: Timestamp when sending notification 103 | /// - dismissalDate: Timestamp when to dismiss live notification when sent with `end`, if in the past 104 | /// dismiss immediately 105 | /// - apnsID: A canonical UUID that identifies the notification. 106 | /// - attributes: The ActivityAttributes of the live activity to start 107 | /// - attributesType: The type name of the ActivityAttributes you want to send 108 | /// - alert: An alert that will be sent along with the notification 109 | public init( 110 | expiration: APNSNotificationExpiration, 111 | priority: APNSPriority, 112 | appID: String, 113 | contentState: ContentState, 114 | timestamp: Int, 115 | dismissalDate: APNSLiveActivityDismissalDate = .none, 116 | apnsID: UUID? = nil, 117 | attributes: Attributes, 118 | attributesType: String, 119 | alert: APNSAlertNotificationContent 120 | ) { 121 | self.aps = APNSStartLiveActivityNotificationAPSStorage( 122 | timestamp: timestamp, 123 | contentState: contentState, 124 | dismissalDate: dismissalDate.dismissal, 125 | alert: alert, 126 | attributes: attributes, 127 | attributesType: attributesType 128 | ) 129 | self.apnsID = apnsID 130 | self.expiration = expiration 131 | self.priority = priority 132 | self.topic = appID + ".push-type.liveactivity" 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/APNSCore/LiveActivity/APNSStartLiveActivityNotificationAPSStorage.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | struct APNSStartLiveActivityNotificationAPSStorage: 16 | Encodable & Sendable 17 | { 18 | enum CodingKeys: String, CodingKey { 19 | case timestamp = "timestamp" 20 | case event = "event" 21 | case contentState = "content-state" 22 | case dismissalDate = "dismissal-date" 23 | case alert = "alert" 24 | case attributes = "attributes" 25 | case attributesType = "attributes-type" 26 | } 27 | 28 | var timestamp: Int 29 | var event: String = "start" 30 | var contentState: ContentState 31 | var dismissalDate: Int? 32 | var alert: APNSAlertNotificationContent 33 | var attributes: Attributes 34 | var attributesType: String 35 | 36 | init( 37 | timestamp: Int, 38 | contentState: ContentState, 39 | dismissalDate: Int?, 40 | alert: APNSAlertNotificationContent, 41 | attributes: Attributes, 42 | attributesType: String 43 | ) { 44 | self.timestamp = timestamp 45 | self.contentState = contentState 46 | self.dismissalDate = dismissalDate 47 | self.alert = alert 48 | self.attributes = attributes 49 | self.attributesType = attributesType 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/APNSCore/Location/APNSClient+Location.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | extension APNSClientProtocol { 16 | /// Sends a location request notification to APNs. 17 | /// 18 | /// - Parameters: 19 | /// - notification: The notification to send. 20 | /// 21 | /// - deviceToken: The hexadecimal bytes that identify the user’s device. Your app receives the bytes for this device token 22 | /// when registering for remote notifications. 23 | /// 24 | /// 25 | /// - logger: The logger to use for sending this notification. 26 | @discardableResult 27 | @inlinable 28 | public func sendLocationNotification( 29 | _ notification: APNSLocationNotification, 30 | deviceToken: String 31 | ) async throws -> APNSResponse { 32 | let request = APNSRequest( 33 | message: notification, 34 | deviceToken: deviceToken, 35 | pushType: .location, 36 | expiration: APNSNotificationExpiration.none, 37 | priority: notification.priority, 38 | apnsID: notification.apnsID, 39 | topic: notification.topic, 40 | collapseID: nil 41 | ) 42 | return try await send(request) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/APNSCore/Location/APNSLocationNotification.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.UUID 16 | 17 | /// A location notification. 18 | public struct APNSLocationNotification: APNSMessage { 19 | /// A canonical UUID that identifies the notification. If there is an error sending the notification, 20 | /// APNs uses this value to identify the notification to your server. The canonical form is 32 lowercase hexadecimal digits, 21 | /// displayed in five groups separated by hyphens in the form 8-4-4-4-12. An example UUID is as follows: 22 | /// `123e4567-e89b-12d3-a456-42665544000`. 23 | /// 24 | /// If you omit this, a new UUID is created by APNs and returned in the response. 25 | public var apnsID: UUID? 26 | 27 | /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.location-query`. 28 | public var topic: String 29 | 30 | /// The priority of the notification. 31 | public var priority: APNSPriority 32 | 33 | /// Initializes a new ``APNSLocationNotification``. 34 | /// 35 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 36 | /// It is **important** that you do not encode anything with the key `aps` 37 | /// 38 | /// - Parameters: 39 | /// - priority: The priority of the notification. 40 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.location-query`. 41 | /// - apnsID: A canonical UUID that identifies the notification. 42 | @inlinable 43 | public init( 44 | priority: APNSPriority, 45 | appID: String, 46 | apnsID: UUID? = nil 47 | ) { 48 | self.init( 49 | priority: priority, 50 | topic: appID + ".location-query", 51 | apnsID: apnsID 52 | ) 53 | } 54 | 55 | /// Initializes a new ``APNSLocationNotification``. 56 | /// 57 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 58 | /// It is **important** that you do not encode anything with the key `aps` 59 | /// 60 | /// - Parameters: 61 | /// - priority: The priority of the notification. 62 | /// - topic: The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.location-query`. 63 | /// - apnsID: A canonical UUID that identifies the notification. 64 | @inlinable 65 | public init( 66 | priority: APNSPriority, 67 | topic: String, 68 | apnsID: UUID? = nil 69 | ) { 70 | self.priority = priority 71 | self.topic = topic 72 | self.apnsID = apnsID 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/APNSCore/Logging.swift: -------------------------------------------------------------------------------- 1 | 2 | enum LoggingKeys { 3 | static let error = "error" 4 | static let authenticationTokenIssuedAt = "authenticationToken.issuedAt" 5 | static let authenticationTokenIssuer = "authenticationToken.issuer" 6 | static let authenticationTokenKeyID = "authenticationToken.keyID" 7 | static let notificationPushType = "notification.pushType" 8 | static let notificationID = "notification.id" 9 | static let notificationExpiration = "notification.expiration" 10 | static let notificationPriority = "notification.priority" 11 | static let notificationTopic = "notification.topic" 12 | static let notificationCollapseID = "notification.collapseID" 13 | } 14 | -------------------------------------------------------------------------------- /Sources/APNSCore/P256.Signing.PrivateKey+PrivateFilePath.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Crypto 16 | 17 | extension P256.Signing.PrivateKey { 18 | public static func loadFrom(string: String) throws -> P256.Signing.PrivateKey { 19 | try .init(pemRepresentation: string) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/APNSCore/PushToTalk/APNSClient+PushToTalk.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | 16 | extension APNSClientProtocol { 17 | /// Sends a Push To Talk (PTT) notification to APNs. 18 | /// 19 | /// - Parameters: 20 | /// - notification: The notification to send. 21 | /// 22 | /// - deviceToken: The hexadecimal bytes that identify the user’s device. Your app receives the bytes for this device token 23 | /// when registering for remote notifications. 24 | /// 25 | /// 26 | /// - logger: The logger to use for sending this notification. 27 | @discardableResult 28 | @inlinable 29 | public func sendPushToTalkNotification( 30 | _ notification: APNSPushToTalkNotification, 31 | deviceToken: String 32 | ) async throws -> APNSResponse { 33 | let request = APNSRequest( 34 | message: notification, 35 | deviceToken: deviceToken, 36 | pushType: .pushtotalk, 37 | expiration: notification.expiration, 38 | priority: notification.priority, 39 | apnsID: notification.apnsID, 40 | topic: notification.topic, 41 | collapseID: nil 42 | ) 43 | return try await send(request) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/APNSCore/PushToTalk/APNSPushToTalkNotification.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.UUID 16 | 17 | /// A Push to Talk (PTT) notification. 18 | public struct APNSPushToTalkNotification: APNSMessage { 19 | /// A canonical UUID that identifies the notification. If there is an error sending the notification, 20 | /// APNs uses this value to identify the notification to your server. The canonical form is 32 lowercase hexadecimal digits, 21 | /// displayed in five groups separated by hyphens in the form 8-4-4-4-12. An example UUID is as follows: 22 | /// `123e4567-e89b-12d3-a456-42665544000`. 23 | /// 24 | /// If you omit this, a new UUID is created by APNs and returned in the response. 25 | public var apnsID: UUID? 26 | 27 | /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.voip-ptt`. 28 | public var topic: String 29 | 30 | /// The date when the notification is no longer valid and can be discarded. If this value is not `none`, 31 | /// APNs stores the notification and tries to deliver it at least once, 32 | /// repeating the attempt as needed if it is unable to deliver the notification the first time. 33 | /// If the value is `immediately`, APNs treats the notification as if it expires immediately 34 | /// and does not store the notification or attempt to redeliver it. 35 | public var expiration: APNSNotificationExpiration 36 | 37 | /// The priority of the notification. 38 | public var priority: APNSPriority 39 | 40 | /// Your custom payload. 41 | public var payload: Payload 42 | 43 | /// Initializes a new ``APNSPushToTalkNotification``. 44 | /// 45 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 46 | /// It is **important** that you do not encode anything with the key `aps` 47 | /// 48 | /// - Parameters: 49 | /// - expiration: The date when the notification is no longer valid and can be discarded. Defaults to `.immediately` 50 | /// - priority: The priority of the notification. Defaults to `.immediately` 51 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.voip-ptt`. 52 | /// - payload: Payload contain speaker data like name of active speaker or indication the session has ended. 53 | /// - apnsID: A canonical UUID that identifies the notification. 54 | @inlinable 55 | public init( 56 | expiration: APNSNotificationExpiration = .immediately, 57 | priority: APNSPriority = .immediately, 58 | appID: String, 59 | payload: Payload, 60 | apnsID: UUID? = nil 61 | ) { 62 | self.init( 63 | expiration: expiration, 64 | priority: priority, 65 | topic: appID + ".voip-ptt", 66 | payload: payload, 67 | apnsID: apnsID 68 | ) 69 | } 70 | 71 | /// Initializes a new ``APNSPushToTalkNotification``. 72 | /// 73 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 74 | /// It is **important** that you do not encode anything with the key `aps` 75 | /// 76 | /// - Parameters: 77 | /// - expiration: The date when the notification is no longer valid and can be discarded. Defaults to `.immediately` 78 | /// - priority: The priority of the notification. Defaults to `.immediately` 79 | /// - topic: The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.voip-ptt`. 80 | /// - payload: Payload contain speaker data like name of active speaker or indication the session has ended. 81 | /// - apnsID: A canonical UUID that identifies the notification. 82 | @inlinable 83 | public init( 84 | expiration: APNSNotificationExpiration = .immediately, 85 | priority: APNSPriority = .immediately, 86 | topic: String, 87 | payload: Payload, 88 | apnsID: UUID? = nil 89 | ) { 90 | self.expiration = expiration 91 | self.priority = priority 92 | self.topic = topic 93 | self.payload = payload 94 | self.apnsID = apnsID 95 | } 96 | } 97 | 98 | extension APNSPushToTalkNotification where Payload == EmptyPayload { 99 | /// Initializes a new ``APNSPushToTalkNotification`` with an EmptyPayload. 100 | /// 101 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 102 | /// It is **important** that you do not encode anything with the key `aps` 103 | /// 104 | /// - Parameters: 105 | /// - expiration: The date when the notification is no longer valid and can be discarded. Defaults to `.immediately` 106 | /// - priority: The priority of the notification. Defaults to `.immediately` 107 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.voip-ptt`. 108 | /// - payload: Empty Payload. 109 | /// - apnsID: A canonical UUID that identifies the notification. 110 | public init( 111 | expiration: APNSNotificationExpiration = .immediately, 112 | priority: APNSPriority = .immediately, 113 | appID: String, 114 | apnsID: UUID? = nil 115 | ) { 116 | self.init( 117 | expiration: expiration, 118 | priority: priority, 119 | topic: appID + ".voip-ptt", 120 | payload: EmptyPayload(), 121 | apnsID: apnsID 122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/APNSCore/VoIP/APNSClient+VoIP.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | 16 | extension APNSClientProtocol { 17 | /// Sends a VoIP notification to APNs. 18 | /// 19 | /// - Parameters: 20 | /// - notification: The notification to send. 21 | /// 22 | /// - deviceToken: The hexadecimal bytes that identify the user’s device. Your app receives the bytes for this device token 23 | /// when registering for remote notifications. 24 | /// 25 | /// 26 | /// - logger: The logger to use for sending this notification. 27 | @discardableResult 28 | @inlinable 29 | public func sendVoIPNotification( 30 | _ notification: APNSVoIPNotification, 31 | deviceToken: String 32 | ) async throws -> APNSResponse { 33 | let request = APNSRequest( 34 | message: notification, 35 | deviceToken: deviceToken, 36 | pushType: .voip, 37 | expiration: notification.expiration, 38 | priority: notification.priority, 39 | apnsID: notification.apnsID, 40 | topic: notification.topic, 41 | collapseID: nil 42 | ) 43 | return try await send(request) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/APNSCore/VoIP/APNSVoIPNotification.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.UUID 16 | 17 | /// A voice-over-IP notification. 18 | public struct APNSVoIPNotification: APNSMessage { 19 | /// A canonical UUID that identifies the notification. If there is an error sending the notification, 20 | /// APNs uses this value to identify the notification to your server. The canonical form is 32 lowercase hexadecimal digits, 21 | /// displayed in five groups separated by hyphens in the form 8-4-4-4-12. An example UUID is as follows: 22 | /// `123e4567-e89b-12d3-a456-42665544000`. 23 | /// 24 | /// If you omit this, a new UUID is created by APNs and returned in the response. 25 | public var apnsID: UUID? 26 | 27 | /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.voip`. 28 | public var topic: String 29 | 30 | /// The date when the notification is no longer valid and can be discarded. If this value is not `none`, 31 | /// APNs stores the notification and tries to deliver it at least once, 32 | /// repeating the attempt as needed if it is unable to deliver the notification the first time. 33 | /// If the value is `immediately`, APNs treats the notification as if it expires immediately 34 | /// and does not store the notification or attempt to redeliver it. 35 | public var expiration: APNSNotificationExpiration 36 | 37 | /// The priority of the notification. 38 | public var priority: APNSPriority 39 | 40 | /// Your custom payload. 41 | public var payload: Payload 42 | 43 | /// Initializes a new ``APNSVoIPNotification``. 44 | /// 45 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 46 | /// It is **important** that you do not encode anything with the key `aps` 47 | /// 48 | /// - Parameters: 49 | /// - expiration: The date when the notification is no longer valid and can be discarded. Defaults to `.immediately` 50 | /// - priority: The priority of the notification. 51 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.voip`. 52 | /// - payload: Your custom payload. 53 | /// - apnsID: A canonical UUID that identifies the notification. 54 | @inlinable 55 | public init( 56 | expiration: APNSNotificationExpiration = .immediately, 57 | priority: APNSPriority, 58 | appID: String, 59 | payload: Payload, 60 | apnsID: UUID? = nil 61 | ) { 62 | self.init( 63 | expiration: expiration, 64 | priority: priority, 65 | topic: appID + ".voip", 66 | payload: payload, 67 | apnsID: apnsID 68 | ) 69 | } 70 | 71 | /// Initializes a new ``APNSVoIPNotification``. 72 | /// 73 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 74 | /// It is **important** that you do not encode anything with the key `aps` 75 | /// 76 | /// - Parameters: 77 | /// - expiration: The date when the notification is no longer valid and can be discarded. Defaults to `.immediately` 78 | /// - priority: The priority of the notification. 79 | /// - topic: The topic for the notification. In general, the topic is your app’s bundle ID/app ID suffixed with `.voip`. 80 | /// - payload: Your custom payload. 81 | /// - apnsID: A canonical UUID that identifies the notification. 82 | @inlinable 83 | public init( 84 | expiration: APNSNotificationExpiration = .immediately, 85 | priority: APNSPriority, 86 | topic: String, 87 | payload: Payload, 88 | apnsID: UUID? = nil 89 | ) { 90 | self.expiration = expiration 91 | self.priority = priority 92 | self.topic = topic 93 | self.payload = payload 94 | self.apnsID = apnsID 95 | } 96 | } 97 | 98 | extension APNSVoIPNotification where Payload == EmptyPayload { 99 | /// Initializes a new ``APNSVoIPNotification`` with an EmptyPayload. 100 | /// 101 | /// - Important: Your dynamic payload will get encoded to the root of the JSON payload that is send to APNs. 102 | /// It is **important** that you do not encode anything with the key `aps` 103 | /// 104 | /// - Parameters: 105 | /// - expiration: The date when the notification is no longer valid and can be discarded. Defaults to `.immediately` 106 | /// - priority: The priority of the notification. 107 | /// - appID: Your app’s bundle ID/app ID. This will be suffixed with `.voip`. 108 | /// - payload: Your custom payload. 109 | /// - apnsID: A canonical UUID that identifies the notification. 110 | public init( 111 | expiration: APNSNotificationExpiration = .immediately, 112 | priority: APNSPriority, 113 | appID: String, 114 | apnsID: UUID? = nil 115 | ) { 116 | self.init( 117 | expiration: expiration, 118 | priority: priority, 119 | topic: appID + ".voip", 120 | payload: EmptyPayload(), 121 | apnsID: apnsID 122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/APNSExample/Program.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | import APNS 17 | import Logging 18 | import Foundation 19 | 20 | let logger = Logger(label: "APNSwiftExample") 21 | 22 | @available(macOS 11.0, *) 23 | @main 24 | struct Main { 25 | /// To use this example app please provide proper values for variable below. 26 | static let deviceToken = "" 27 | static let pushKitDeviceToken = "" 28 | static let ephemeralPushToken = "" // PTT 29 | static let fileProviderDeviceToken = "" 30 | static let appBundleID = "" 31 | static let privateKey = """ 32 | """ 33 | static let keyIdentifier = "" 34 | static let teamIdentifier = "" 35 | 36 | 37 | static func main() async throws { 38 | 39 | let client = APNSClient( 40 | configuration: .init( 41 | authenticationMethod: .jwt( 42 | privateKey: try .init(pemRepresentation: privateKey), 43 | keyIdentifier: keyIdentifier, 44 | teamIdentifier: teamIdentifier 45 | ), 46 | environment: .development 47 | ), 48 | eventLoopGroupProvider: .createNew, 49 | responseDecoder: JSONDecoder(), 50 | requestEncoder: JSONEncoder() 51 | ) 52 | 53 | do { 54 | try await Self.sendSimpleAlert(with: client) 55 | try await Self.sendLocalizedAlert(with: client) 56 | try await Self.sendThreadedAlert(with: client) 57 | try await Self.sendCustomCategoryAlert(with: client) 58 | try await Self.sendMutableContentAlert(with: client) 59 | try await Self.sendBackground(with: client) 60 | try await Self.sendVoIP(with: client) 61 | try await Self.sendFileProvider(with: client) 62 | try await Self.sendPushToTalk(with: client) 63 | } catch { 64 | logger.warning("error sending push: \(error)") 65 | } 66 | 67 | try? await client.shutdown() 68 | } 69 | } 70 | 71 | // MARK: Alerts 72 | 73 | @available(macOS 11.0, *) 74 | extension Main { 75 | static func sendSimpleAlert(with client: some APNSClientProtocol) async throws { 76 | try await client.sendAlertNotification( 77 | .init( 78 | alert: .init( 79 | title: .raw("Simple Alert"), 80 | subtitle: .raw("Subtitle"), 81 | body: .raw("Body"), 82 | launchImage: nil 83 | ), 84 | expiration: .immediately, 85 | priority: .immediately, 86 | topic: self.appBundleID, 87 | payload: EmptyPayload() 88 | ), 89 | deviceToken: self.deviceToken 90 | ) 91 | logger.info("successfully sent simple alert notification") 92 | } 93 | 94 | static func sendLocalizedAlert(with client: some APNSClientProtocol) async throws { 95 | try await client.sendAlertNotification( 96 | .init( 97 | alert: .init( 98 | title: .localized(key: "title", arguments: ["Localized"]), 99 | subtitle: .localized(key: "subtitle", arguments: ["APNS"]), 100 | body: .localized(key: "body", arguments: ["APNS"]), 101 | launchImage: nil 102 | ), 103 | expiration: .immediately, 104 | priority: .immediately, 105 | topic: self.appBundleID, 106 | payload: EmptyPayload() 107 | ), 108 | deviceToken: self.deviceToken 109 | ) 110 | logger.info("successfully sent alert localized notification") 111 | } 112 | 113 | static func sendThreadedAlert(with client: some APNSClientProtocol) async throws { 114 | try await client.sendAlertNotification( 115 | .init( 116 | alert: .init( 117 | title: .raw("Threaded Alert"), 118 | subtitle: .raw("Subtitle"), 119 | body: .raw("Body"), 120 | launchImage: nil 121 | ), 122 | expiration: .immediately, 123 | priority: .immediately, 124 | topic: self.appBundleID, 125 | payload: EmptyPayload(), 126 | threadID: "thread" 127 | ), 128 | deviceToken: self.deviceToken 129 | ) 130 | logger.info("successfully sent threaded alert") 131 | } 132 | 133 | static func sendCustomCategoryAlert(with client: some APNSClientProtocol) async throws { 134 | try await client.sendAlertNotification( 135 | .init( 136 | alert: .init( 137 | title: .raw("Custom Category Alert"), 138 | subtitle: .raw("Subtitle"), 139 | body: .raw("Body"), 140 | launchImage: nil 141 | ), 142 | expiration: .immediately, 143 | priority: .immediately, 144 | topic: self.appBundleID, 145 | payload: EmptyPayload(), 146 | category: "CUSTOM" 147 | ), 148 | deviceToken: self.deviceToken 149 | ) 150 | logger.info("successfully sent custom category alert") 151 | } 152 | 153 | static func sendMutableContentAlert(with client: some APNSClientProtocol) async throws { 154 | try await client.sendAlertNotification( 155 | .init( 156 | alert: .init( 157 | title: .raw("Mutable Alert"), 158 | subtitle: .raw("Subtitle"), 159 | body: .raw("Body"), 160 | launchImage: nil 161 | ), 162 | expiration: .immediately, 163 | priority: .immediately, 164 | topic: self.appBundleID, 165 | payload: EmptyPayload(), 166 | mutableContent: 1 167 | ), 168 | deviceToken: self.deviceToken 169 | ) 170 | logger.info("successfully sent mutable content alert") 171 | } 172 | } 173 | 174 | // MARK: Background 175 | 176 | @available(macOS 11.0, *) 177 | extension Main { 178 | static func sendBackground(with client: some APNSClientProtocol) async throws { 179 | try await client.sendBackgroundNotification( 180 | .init( 181 | expiration: .immediately, 182 | topic: self.appBundleID, 183 | payload: EmptyPayload() 184 | ), 185 | deviceToken: self.deviceToken 186 | ) 187 | logger.info("successfully sent background notification") 188 | } 189 | } 190 | 191 | // MARK: VoIP 192 | 193 | @available(macOS 11.0, *) 194 | extension Main { 195 | static func sendVoIP(with client: some APNSClientProtocol) async throws { 196 | try await client.sendVoIPNotification( 197 | .init( 198 | expiration: .immediately, 199 | priority: .immediately, 200 | appID: self.appBundleID, 201 | payload: EmptyPayload() 202 | ), 203 | deviceToken: self.pushKitDeviceToken 204 | ) 205 | logger.info("successfully sent VoIP notification") 206 | } 207 | } 208 | 209 | // MARK: FileProvider 210 | 211 | @available(macOS 11.0, *) 212 | extension Main { 213 | static func sendFileProvider(with client: some APNSClientProtocol) async throws { 214 | try await client.sendFileProviderNotification( 215 | .init( 216 | expiration: .immediately, 217 | appID: self.appBundleID, 218 | payload: EmptyPayload() 219 | ), 220 | deviceToken: self.fileProviderDeviceToken 221 | ) 222 | logger.info("successfully sent FileProvider notification") 223 | } 224 | } 225 | 226 | 227 | // MARK: Push to Talk (PTT) 228 | 229 | @available(macOS 11.0, *) 230 | extension Main { 231 | static func sendPushToTalk(with client: some APNSClientProtocol) async throws { 232 | try await client.sendPushToTalkNotification( 233 | .init( 234 | expiration: .immediately, 235 | priority: .immediately, 236 | appID: self.appBundleID, 237 | payload: EmptyPayload() 238 | ), 239 | deviceToken: self.ephemeralPushToken 240 | ) 241 | logger.info("successfully sent Push to Talk notification") 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Sources/APNSTestServer/APNSTestServer.swift: -------------------------------------------------------------------------------- 1 | struct APNSTestServer {} 2 | -------------------------------------------------------------------------------- /Sources/APNSURLSession/APNSURLSessionClientConfiguration.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | @preconcurrency import Crypto 17 | 18 | /// The configuration of an ``APNSURLSessionClient``. 19 | public struct APNSURLSessionClientConfiguration { 20 | /// The authentication method used by the ``APNSURLSessionClient``. 21 | public enum AuthenticationMethod { 22 | case jwt(privateKey: P256.Signing.PrivateKey, teamIdentifier: String, keyIdentifier: String) 23 | } 24 | 25 | /// The authentication method used by the ``APNSURLSessionClient``. 26 | public var authenticationMethod: AuthenticationMethod 27 | 28 | /// The environment used by the ``APNSURLSessionClient``. 29 | public var environment: APNSEnvironment 30 | 31 | private let authenticationTokenManager: APNSAuthenticationTokenManager 32 | 33 | internal func nextValidToken() async throws -> String { 34 | try await authenticationTokenManager.nextValidToken 35 | } 36 | 37 | /// Initializes a new ``APNSClient.Configuration``. 38 | /// 39 | /// - Parameters: 40 | /// - environment: The environment used by the ``APNSURLSessionClient``. 41 | /// - privateKey: The private encryption key obtained through the developer portal. 42 | /// - keyIdentifier: The private encryption key identifier obtained through the developer portal. 43 | /// - teamIdentifier: The team id. 44 | public init( 45 | environment: APNSEnvironment, 46 | privateKey: P256.Signing.PrivateKey, 47 | keyIdentifier: String, 48 | teamIdentifier: String, 49 | clock: any Clock = ContinuousClock() 50 | ) { 51 | self.authenticationMethod = .jwt(privateKey: privateKey, teamIdentifier: teamIdentifier, keyIdentifier: keyIdentifier) 52 | self.environment = environment 53 | 54 | self.authenticationTokenManager = APNSAuthenticationTokenManager( 55 | privateKey: privateKey, 56 | teamIdentifier: teamIdentifier, 57 | keyIdentifier: keyIdentifier, 58 | clock: ContinuousClock() 59 | ) 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /Sources/APNSURLSession/APNSUrlSessionClient.swift: -------------------------------------------------------------------------------- 1 | import APNSCore 2 | import Foundation 3 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 4 | 5 | enum APNSUrlSessionClientError: Error { 6 | case urlResponseNotFound 7 | } 8 | 9 | public struct APNSURLSessionClient: APNSClientProtocol { 10 | 11 | private let configuration: APNSURLSessionClientConfiguration 12 | 13 | let encoder = JSONEncoder() 14 | let decoder = JSONDecoder() 15 | 16 | public init(configuration: APNSURLSessionClientConfiguration) { 17 | self.configuration = configuration 18 | } 19 | 20 | public func send( 21 | _ request: APNSRequest 22 | ) async throws -> APNSResponse { 23 | 24 | /// Construct URL 25 | var urlRequest = URLRequest(url: URL(string: configuration.environment.absoluteURL + "/\(request.deviceToken)")!) 26 | urlRequest.httpMethod = "POST" 27 | /// Set headers 28 | urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") 29 | for (header, value) in request.headers { 30 | urlRequest.setValue(value, forHTTPHeaderField: header) 31 | } 32 | 33 | await urlRequest.setValue(try configuration.nextValidToken(), forHTTPHeaderField: "Authorization") 34 | 35 | /// Set Body 36 | urlRequest.httpBody = try encoder.encode(request.message) 37 | 38 | /// Make request 39 | let (data, response) = try await URLSession.shared.data(for: urlRequest) 40 | 41 | /// Unwrap response 42 | guard let response = response as? HTTPURLResponse, 43 | let apnsIDString = response.allHeaderFields["apns-id"] as? String else { 44 | throw APNSUrlSessionClientError.urlResponseNotFound 45 | } 46 | 47 | let apnsID = UUID(uuidString: apnsIDString) 48 | let apnsUniqueID = (response.allHeaderFields["apns-unique-id"] as? String).flatMap { UUID(uuidString: $0) } 49 | 50 | /// Detect an error 51 | if let errorResponse = try? decoder.decode(APNSErrorResponse.self, from: data) { 52 | let error = APNSError( 53 | responseStatus: response.statusCode, 54 | apnsID: apnsID, 55 | apnsUniqueID: apnsUniqueID, 56 | apnsResponse: errorResponse, 57 | timestamp: errorResponse.timestampInSeconds.flatMap { Date(timeIntervalSince1970: $0) } 58 | ) 59 | throw error 60 | } else { 61 | /// Return APNSResponse 62 | return APNSResponse(apnsID: apnsID, apnsUniqueID: apnsUniqueID) 63 | } 64 | } 65 | 66 | public func shutdown() async throws { 67 | // no op 68 | } 69 | } 70 | 71 | #endif 72 | -------------------------------------------------------------------------------- /Tests/APNSTests/APNSAuthenticationTokenManagerTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | @testable import APNSCore 16 | import Crypto 17 | import XCTest 18 | import NIOConcurrencyHelpers 19 | 20 | final class APNSAuthenticationTokenManagerTests: XCTestCase { 21 | private static let signingKey = """ 22 | -----BEGIN EC PRIVATE KEY----- 23 | MHcCAQEEIPnrjgMs/LOp9W5R2kQtdBfzyjCe2wICBOWgyCA6OwRDoAoGCCqGSM49 24 | AwEHoUQDQgAEbWmxH/HLvIJIVUt8bB42ntiBZUSb6Bxx7F36mDSHssBaRBU0BYYj 25 | NVeBKbgP2rVE/nOAexjhmWE2S5G98nkEPg== 26 | -----END EC PRIVATE KEY----- 27 | 28 | """ 29 | private var clock: TestClock! 30 | private var tokenManager: APNSAuthenticationTokenManager>! 31 | 32 | override func setUp() { 33 | super.setUp() 34 | clock = TestClock() 35 | tokenManager = APNSAuthenticationTokenManager( 36 | privateKey: try! .init(pemRepresentation: Self.signingKey), 37 | teamIdentifier: "foo", 38 | keyIdentifier: "bar", 39 | clock: clock 40 | ) 41 | } 42 | 43 | override func tearDown() { 44 | super.tearDown() 45 | 46 | tokenManager = nil 47 | clock = nil 48 | } 49 | 50 | func testToken() async throws { 51 | let token = try await tokenManager.nextValidToken 52 | 53 | // We need to split twice here since the expected format of the token is 54 | // "bearer encodedHeader.encodedPayload.ecnodedSignature" 55 | let splitToken = try XCTUnwrap(token.split(separator: " ").last) 56 | .split(separator: ".") 57 | 58 | let decodedHeader = try Base64.decode( 59 | string: String(splitToken[0]), 60 | options: [.base64UrlAlphabet, .omitPaddingCharacter] 61 | ) 62 | let header = String(bytes: decodedHeader, encoding: .utf8) 63 | let expectedHeader = """ 64 | { 65 | "alg": "ES256", 66 | "typ": "JWT", 67 | "kid": "bar" 68 | } 69 | """ 70 | XCTAssertEqual(header, expectedHeader) 71 | 72 | let decodedPayload = try Base64.decode( 73 | string: String(splitToken[1]), 74 | options: [.base64UrlAlphabet, .omitPaddingCharacter] 75 | ) 76 | let payload = String(bytes: decodedPayload, encoding: .utf8) 77 | let issuedAtTime = DispatchWallTime.now() 78 | let expectedPayload = """ 79 | { 80 | "iss": "foo", 81 | "iat": "\(issuedAtTime.asSecondsSince1970)", 82 | "kid": "bar" 83 | } 84 | """ 85 | XCTAssertEqual(payload, expectedPayload) 86 | } 87 | 88 | func testTokenIsReused() async throws { 89 | 90 | let token1 = try await tokenManager.nextValidToken 91 | // 48 minutes later 92 | let temp = clock.now.advanced(by: .init(secondsComponent: 2880, attosecondsComponent: 0)) 93 | clock.now = temp 94 | let token2 = try await tokenManager.nextValidToken 95 | 96 | XCTAssertEqual(token1, token2) 97 | } 98 | 99 | func testTokenIsRefreshed() async throws { 100 | let token1 = try await tokenManager.nextValidToken 101 | 102 | // 56 minutes later 103 | let temp = clock.now.advanced(by: .init(secondsComponent: 3360, attosecondsComponent: 0)) 104 | clock.now = temp 105 | let token2 = try await tokenManager.nextValidToken 106 | 107 | XCTAssertNotEqual(token1, token2) 108 | } 109 | } 110 | 111 | final class TestClock: Clock { 112 | struct Instant: InstantProtocol { 113 | public var offset: Duration 114 | 115 | public init(offset: Duration = .zero) { 116 | self.offset = offset 117 | } 118 | 119 | public func advanced(by duration: Duration) -> Self { 120 | .init(offset: self.offset + duration) 121 | } 122 | 123 | public func duration(to other: Self) -> Duration { 124 | other.offset - self.offset 125 | } 126 | 127 | public static func < (lhs: Self, rhs: Self) -> Bool { 128 | lhs.offset < rhs.offset 129 | } 130 | } 131 | 132 | let minimumResolution: Duration = .zero 133 | private let _now: NIOLockedValueBox 134 | 135 | var now: Instant { 136 | get { 137 | self._now.withLockedValue { $0 } 138 | } set { 139 | self._now.withLockedValue { $0 = newValue } 140 | } 141 | } 142 | 143 | 144 | public init(now: Instant = .init()) { 145 | self._now = .init(now) 146 | } 147 | 148 | public func sleep(until deadline: Instant, tolerance: Duration? = nil) async throws { 149 | try Task.checkCancellation() 150 | try await Task.sleep(until: deadline, clock: self) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Tests/APNSTests/APNSClientTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | @testable import APNSCore 16 | import APNS 17 | import Crypto 18 | import XCTest 19 | 20 | final class APNSClientTests: XCTestCase { 21 | func testShutdown() async throws { 22 | let client = self.makeClient() 23 | try await client.shutdown() 24 | } 25 | 26 | // MARK: - Helper methods 27 | 28 | private func makeClient() -> APNSClient { 29 | APNSClient( 30 | configuration: .init( 31 | authenticationMethod: .jwt( 32 | privateKey: try! P256.Signing.PrivateKey(pemRepresentation: self.jwtPrivateKey), 33 | keyIdentifier: "MY_KEY_ID", 34 | teamIdentifier: "MY_TEAM_ID" 35 | ), 36 | environment: .development 37 | ), 38 | eventLoopGroupProvider: .createNew, 39 | responseDecoder: JSONDecoder(), 40 | requestEncoder: JSONEncoder() 41 | ) 42 | } 43 | 44 | private let jwtPrivateKey = """ 45 | -----BEGIN PRIVATE KEY----- 46 | MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg2sD+kukkA8GZUpmm 47 | jRa4fJ9Xa/JnIG4Hpi7tNO66+OGgCgYIKoZIzj0DAQehRANCAATZp0yt0btpR9kf 48 | ntp4oUUzTV0+eTELXxJxFvhnqmgwGAm1iVW132XLrdRG/ntlbQ1yzUuJkHtYBNve 49 | y+77Vzsd 50 | -----END PRIVATE KEY----- 51 | """ 52 | } 53 | 54 | // This doesn't perform any runtime tests, it just ensures the call to sendAlertNotification 55 | // compiles when called within an actor's isolation context. 56 | actor TestActor { 57 | func sendAlert(client: APNSClient) async throws { 58 | let notification = APNSAlertNotification( 59 | alert: .init(title: .raw("title")), 60 | expiration: .immediately, 61 | priority: .immediately, 62 | topic: "", 63 | payload: "" 64 | ) 65 | try await client.sendAlertNotification(notification, deviceToken: "") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/APNSTests/Alert/APNSAlertNotificationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | import XCTest 17 | 18 | final class APNSAlertNotificationTests: XCTestCase { 19 | func testEncode() throws { 20 | struct Payload: Encodable { 21 | let foo = "bar" 22 | } 23 | let notification = APNSAlertNotification( 24 | alert: .init(title: .raw("title")), 25 | expiration: .immediately, 26 | priority: .immediately, 27 | topic: "", 28 | payload: Payload() 29 | ) 30 | let encoder = JSONEncoder() 31 | let data = try encoder.encode(notification) 32 | 33 | let expectedJSONString = """ 34 | {"foo":"bar","aps":{"alert":{"title":"title"}}} 35 | """ 36 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 37 | let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary 38 | XCTAssertEqual(jsonObject1, jsonObject2) 39 | } 40 | 41 | func testEncode_whenAPSKeyInPayload() throws { 42 | struct Payload: Encodable { 43 | let aps = "foo" 44 | } 45 | let notification = APNSAlertNotification( 46 | alert: .init(title: .raw("title")), 47 | expiration: .immediately, 48 | priority: .immediately, 49 | topic: "", 50 | payload: Payload() 51 | ) 52 | let encoder = JSONEncoder() 53 | let data = try encoder.encode(notification) 54 | 55 | let expectedJSONString = """ 56 | {"aps":{"alert":{"title":"title"}}} 57 | """ 58 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 59 | let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary 60 | XCTAssertEqual(jsonObject1, jsonObject2) 61 | } 62 | 63 | func testEncode_whenDefaultSound() throws { 64 | struct Payload: Encodable { 65 | let payload = "payload" 66 | } 67 | let notification = APNSAlertNotification( 68 | alert: .init( 69 | title: .raw("title"), 70 | subtitle: .localized( 71 | key: "subtitle-key", 72 | arguments: ["arg1"] 73 | ), 74 | body: .raw("body"), 75 | launchImage: "launchimage" 76 | ), 77 | expiration: .timeIntervalSince1970InSeconds(1_652_693_147), 78 | priority: .consideringDevicePower, 79 | topic: "topic", 80 | payload: Payload(), 81 | badge: 1, 82 | sound: .default, 83 | threadID: "threadID", 84 | category: "category", 85 | mutableContent: 1, 86 | targetContentID: "targetContentID", 87 | interruptionLevel: .critical, 88 | relevanceScore: 1, 89 | apnsID: .init() 90 | ) 91 | let encoder = JSONEncoder() 92 | let data = try encoder.encode(notification) 93 | 94 | let expectedJSONString = """ 95 | {\"payload\":\"payload\",\"aps\":{\"category\":\"category\",\"relevance-score\":1,\"badge\":1,\"target-content-id\":\"targetContentID\",\"sound\":\"default\",\"interruption-level\":\"critical\",\"alert\":{\"body\":\"body\",\"subtitle-loc-key\":\"subtitle-key\",\"title\":\"title\",\"launch-image\":\"launchimage\",\"subtitle-loc-args\":[\"arg1\"]},\"thread-id\":\"threadID\",\"mutable-content\":1}} 96 | """ 97 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 98 | let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary 99 | XCTAssertEqual(jsonObject1, jsonObject2) 100 | } 101 | 102 | func testEncode_whenCriticalSound() throws { 103 | struct Payload: Encodable { 104 | let payload = "payload" 105 | } 106 | let notification = APNSAlertNotification( 107 | alert: .init( 108 | title: .raw("title"), 109 | subtitle: .localized( 110 | key: "subtitle-key", 111 | arguments: ["arg1"] 112 | ), 113 | body: .raw("body"), 114 | launchImage: "launchimage" 115 | ), 116 | expiration: .timeIntervalSince1970InSeconds(1_652_693_147), 117 | priority: .consideringDevicePower, 118 | topic: "topic", 119 | payload: Payload(), 120 | badge: 1, 121 | sound: .critical(fileName: "file", volume: 1), 122 | threadID: "threadID", 123 | category: "category", 124 | mutableContent: 1, 125 | targetContentID: "targetContentID", 126 | interruptionLevel: .critical, 127 | relevanceScore: 1, 128 | apnsID: .init() 129 | ) 130 | let encoder = JSONEncoder() 131 | let data = try encoder.encode(notification) 132 | 133 | let expectedJSONString = """ 134 | {\"payload\":\"payload\",\"aps\":{\"category\":\"category\",\"relevance-score\":1,\"badge\":1,\"target-content-id\":\"targetContentID\",\"sound\":{\"name\":\"file\",\"volume\":1,\"critical\":1},\"interruption-level\":\"critical\",\"alert\":{\"body\":\"body\",\"subtitle-loc-key\":\"subtitle-key\",\"title\":\"title\",\"launch-image\":\"launchimage\",\"subtitle-loc-args\":[\"arg1\"]},\"thread-id\":\"threadID\",\"mutable-content\":1}} 135 | """ 136 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 137 | let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary 138 | XCTAssertEqual(jsonObject1, jsonObject2) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Tests/APNSTests/Background/APNSBackgroundNotificationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | import XCTest 17 | 18 | final class APNSBackgroundNotificationTests: XCTestCase { 19 | func testEncode() throws { 20 | struct Payload: Encodable { 21 | let foo = "bar" 22 | } 23 | let notification = APNSBackgroundNotification( 24 | expiration: .none, 25 | topic: "com.test.app", 26 | payload: Payload(), 27 | apnsID: nil 28 | ) 29 | 30 | let encoder = JSONEncoder() 31 | let data = try encoder.encode(notification) 32 | 33 | let expectedJSONString = """ 34 | {"foo":"bar","aps":{"content-available":1}} 35 | """ 36 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 37 | let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary 38 | XCTAssertEqual(jsonObject1, jsonObject2) 39 | } 40 | 41 | func testEnode_whenAPSKeyInPayload() throws { 42 | struct Payload: Encodable { 43 | let aps = "foo" 44 | } 45 | let notification = APNSBackgroundNotification( 46 | expiration: .none, 47 | topic: "com.test.app", 48 | payload: Payload(), 49 | apnsID: nil 50 | ) 51 | 52 | let encoder = JSONEncoder() 53 | let data = try encoder.encode(notification) 54 | 55 | let expectedJSONString = """ 56 | {"aps":{"content-available":1}} 57 | """ 58 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 59 | let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary 60 | XCTAssertEqual(jsonObject1, jsonObject2) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/APNSTests/Complication/APNSComplicationNotificationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | import XCTest 17 | 18 | final class APNSComplicationNotificationTests: XCTestCase { 19 | func testAppID() { 20 | struct Payload: Encodable { 21 | let foo = "bar" 22 | } 23 | let complicationNotification = APNSComplicationNotification( 24 | expiration: .immediately, 25 | priority: .immediately, 26 | appID: "com.example.app", 27 | payload: Payload() 28 | ) 29 | 30 | XCTAssertEqual(complicationNotification.topic, "com.example.app.complication") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/APNSTests/FileProvider/APNSFileProviderNotificationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | import XCTest 17 | 18 | final class APNSFileProviderNotificationTests: XCTestCase { 19 | func testAppID() { 20 | struct Payload: Encodable { 21 | let foo = "bar" 22 | } 23 | let fileProviderNotification = APNSFileProviderNotification( 24 | expiration: .immediately, 25 | appID: "com.example.app", 26 | payload: Payload() 27 | ) 28 | 29 | XCTAssertEqual(fileProviderNotification.topic, "com.example.app.pushkit.fileprovider") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/APNSTests/LiveActivity/APNSLiveActivityNotificationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | import XCTest 17 | 18 | final class APNSLiveActivityNotificationTests: XCTestCase { 19 | 20 | struct Attributes: Encodable { 21 | let name: String = "Test Attribute" 22 | } 23 | 24 | struct State: Encodable, Hashable { 25 | let string: String = "Test" 26 | let number: Int = 123 27 | } 28 | 29 | func testEncodeUpdate() throws { 30 | let notification = APNSLiveActivityNotification( 31 | expiration: .immediately, 32 | priority: .immediately, 33 | appID: "test.app.id", 34 | contentState: State(), 35 | event: .update, 36 | timestamp: 1_672_680_658) 37 | 38 | let encoder = JSONEncoder() 39 | let data = try encoder.encode(notification) 40 | 41 | let expectedJSONString = """ 42 | {"aps":{"event":"update","content-state":{"string":"Test","number":123},"timestamp":1672680658}} 43 | """ 44 | 45 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 46 | let jsonObject2 = 47 | try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) 48 | as! NSDictionary 49 | XCTAssertEqual(jsonObject1, jsonObject2) 50 | } 51 | 52 | func testEncodeUpdateAlert() throws { 53 | let notification = APNSLiveActivityNotification( 54 | expiration: .immediately, 55 | priority: .immediately, 56 | appID: "test.app.id", 57 | contentState: State(), 58 | event: .update, 59 | alert: .init(title: .raw("Hi"), body: .raw("Hello")), 60 | timestamp: 1_672_680_658 61 | ) 62 | 63 | let encoder = JSONEncoder() 64 | let data = try encoder.encode(notification) 65 | 66 | let expectedJSONString = """ 67 | {"aps":{"event":"update", "alert": { "title": "Hi", "body": "Hello" },"content-state":{"string":"Test","number":123},"timestamp":1672680658}} 68 | """ 69 | 70 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 71 | let jsonObject2 = 72 | try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) 73 | as! NSDictionary 74 | XCTAssertEqual(jsonObject1, jsonObject2) 75 | } 76 | 77 | func testEncodeStart() throws { 78 | let notification = APNSStartLiveActivityNotification( 79 | expiration: .immediately, 80 | priority: .immediately, 81 | appID: "test.app.id", 82 | contentState: State(), 83 | timestamp: 1_672_680_658, 84 | attributes: Attributes(), 85 | attributesType: "Attributes", 86 | alert: .init(title: .raw("Hi"), body: .raw("Hello")) 87 | ) 88 | 89 | let encoder = JSONEncoder() 90 | let data = try encoder.encode(notification) 91 | 92 | let expectedJSONString = """ 93 | {"aps":{"event":"start", "alert": { "title": "Hi", "body": "Hello" }, "attributes-type": "Attributes", "attributes": {"name":"Test Attribute"},"content-state":{"string":"Test","number":123},"timestamp":1672680658}} 94 | """ 95 | 96 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 97 | let jsonObject2 = 98 | try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) 99 | as! NSDictionary 100 | XCTAssertEqual(jsonObject1, jsonObject2) 101 | } 102 | 103 | func testEncodeEndNoDismiss() throws { 104 | let notification = APNSLiveActivityNotification( 105 | expiration: .immediately, 106 | priority: .immediately, 107 | appID: "test.app.id", 108 | contentState: State(), 109 | event: .end, 110 | timestamp: 1_672_680_658) 111 | 112 | let encoder = JSONEncoder() 113 | let data = try encoder.encode(notification) 114 | 115 | let expectedJSONString = """ 116 | {"aps":{"event":"end","content-state":{"string":"Test","number":123},"timestamp":1672680658}} 117 | """ 118 | 119 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 120 | let jsonObject2 = 121 | try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) 122 | as! NSDictionary 123 | XCTAssertEqual(jsonObject1, jsonObject2) 124 | } 125 | 126 | func testEncodeEndDismiss() throws { 127 | let notification = APNSLiveActivityNotification( 128 | expiration: .immediately, 129 | priority: .immediately, 130 | appID: "test.app.id", 131 | contentState: State(), 132 | event: .end, 133 | timestamp: 1_672_680_658, 134 | dismissalDate: .timeIntervalSince1970InSeconds(1_672_680_800)) 135 | 136 | let encoder = JSONEncoder() 137 | let data = try encoder.encode(notification) 138 | 139 | let expectedJSONString = """ 140 | {"aps":{"event":"end","content-state":{"string":"Test","number":123},"timestamp":1672680658, 141 | "dismissal-date":1672680800}} 142 | """ 143 | 144 | let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary 145 | let jsonObject2 = 146 | try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) 147 | as! NSDictionary 148 | XCTAssertEqual(jsonObject1, jsonObject2) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Tests/APNSTests/PushToTalk/APNSPushToTalkNotificationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | import XCTest 17 | 18 | final class APNSPushToTalkNotificationTests: XCTestCase { 19 | func testAppID() { 20 | struct Payload: Encodable { 21 | let foo = "bar" 22 | } 23 | let voipNotification = APNSPushToTalkNotification( 24 | appID: "com.example.app", 25 | payload: Payload() 26 | ) 27 | 28 | XCTAssertEqual(voipNotification.topic, "com.example.app.voip-ptt") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/APNSTests/VoIP/APNSVoIPNotificationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the APNSwift open source project 4 | // 5 | // Copyright (c) 2022 the APNSwift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of APNSwift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import APNSCore 16 | import XCTest 17 | 18 | final class APNSVoIPNotificationTests: XCTestCase { 19 | func testAppID() { 20 | struct Payload: Encodable { 21 | let foo = "bar" 22 | } 23 | let voipNotification = APNSVoIPNotification( 24 | priority: .immediately, 25 | appID: "com.example.app", 26 | payload: Payload() 27 | ) 28 | 29 | XCTAssertEqual(voipNotification.topic, "com.example.app.voip") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scripts/generate_contributors_list.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the APNSwift open source project 5 | ## 6 | ## Copyright (c) 2022 the APNSwift project authors 7 | ## Licensed under Apache License v2.0 8 | ## 9 | ## See LICENSE.txt for license information 10 | ## See CONTRIBUTORS.txt for the list of N project authors 11 | ## 12 | ## SPDX-License-Identifier: Apache-2.0 13 | ## 14 | ##===----------------------------------------------------------------------===## 15 | 16 | set -eu 17 | here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 18 | contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) 19 | 20 | cat > "$here/../CONTRIBUTORS.txt" <<- EOF 21 | For the purpose of tracking copyright, this is the list of individuals and 22 | organizations who have contributed source code to APNSwift. 23 | 24 | For employees of an organization/company where the copyright of work done 25 | by employees of that company is held by the company itself, only the company 26 | needs to be listed here. 27 | 28 | ## COPYRIGHT HOLDERS 29 | 30 | ### Contributors 31 | 32 | $contributors 33 | 34 | **Updating this list** 35 | 36 | Please do not edit this file manually. It is generated using \`./scripts/generate_contributors_list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\` 37 | EOF 38 | -------------------------------------------------------------------------------- /scripts/linux_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # -e: exit when a command fails 4 | # -o pipefail: set exit status of shell script to last nonzero exit code, if any were nonzero. 5 | set -o pipefail 6 | 7 | echo "" 8 | echo "Running Tests in Docker Container" 9 | echo "Swift 5" 10 | echo "=================================" 11 | docker rmi swift-nio-apns 12 | docker build -t swift-nio-apns -f Dockerfile . 13 | 14 | docker run --name swift-nio-apns-tests --rm swift-nio-apns swift test \ 15 | || (set +x; echo -e "\033[0;31mTests exited with non-zero exit code\033[0m"; tput bel; exit 1) 16 | echo "Finished tests, docker container were removed." --------------------------------------------------------------------------------