Skip to content

Commit dedccc1

Browse files
Container Identifiers (#58)
1 parent d00f13b commit dedccc1

File tree

3 files changed

+194
-1
lines changed

3 files changed

+194
-1
lines changed
+20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11

22
extension APNSContainers.ID {
3+
/// A default container ID available for use.
4+
///
5+
/// If you are configuring both a production and development container, ``production`` and ``development`` are also available.
6+
///
7+
/// - Note: You must configure this ID before using it by calling ``VaporAPNS/APNSContainers/use(_:eventLoopGroupProvider:responseDecoder:requestEncoder:byteBufferAllocator:as:isDefault:)``.
8+
/// - Important: The actual default ID to use in ``Vapor/Application/APNS/client`` when none is provided is the first configuration that doesn't specify a value of `false` for `isDefault:`.
39
public static var `default`: APNSContainers.ID {
410
return .init(string: "default")
511
}
12+
13+
/// An ID that can be used for the production APNs environment.
14+
///
15+
/// - Note: You must configure this ID before using it by calling ``APNSContainers/use(_:eventLoopGroupProvider:responseDecoder:requestEncoder:byteBufferAllocator:as:isDefault:)``
16+
public static var production: APNSContainers.ID {
17+
return .init(string: "production")
18+
}
19+
20+
/// An ID that can be used for the development (aka sandbox) APNs environment.
21+
///
22+
/// - Note: You must configure this ID before using it by calling ``APNSContainers/use(_:eventLoopGroupProvider:responseDecoder:requestEncoder:byteBufferAllocator:as:isDefault:)``
23+
public static var development: APNSContainers.ID {
24+
return .init(string: "development")
25+
}
626
}

Sources/VaporAPNS/APNSContainers.swift

+112-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,118 @@ public final class APNSContainers: Sendable {
5050
}
5151

5252
extension APNSContainers {
53-
53+
/// Configure APNs for a given container ID.
54+
///
55+
/// You must configure at lease one client in order to send notifications to devices. If you plan on supporting both development builds (ie. run from Xcode) and release builds (ie. TestFlight/App Store), you must configure at least two configurations:
56+
///
57+
/// ```swift
58+
/// /// The .p8 file as a string.
59+
/// let apnsKey = Environment.get("APNS_KEY_P8")
60+
/// /// The identifier of the key in the developer portal.
61+
/// let keyIdentifier = Environment.get("APNS_KEY_ID")
62+
/// /// The team identifier of the app in the developer portal.
63+
/// let teamIdentifier = Environment.get("APNS_TEAM_ID")
64+
///
65+
/// let productionConfig = APNSClientConfiguration(
66+
/// authenticationMethod: .jwt(
67+
/// privateKey: try .loadFrom(string: apnsKey),
68+
/// keyIdentifier: keyIdentifier,
69+
/// teamIdentifier: teamIdentifier
70+
/// ),
71+
/// environment: .production
72+
/// )
73+
///
74+
/// app.apns.containers.use(
75+
/// productionConfig,
76+
/// eventLoopGroupProvider: .shared(app.eventLoopGroup),
77+
/// responseDecoder: JSONDecoder(),
78+
/// requestEncoder: JSONEncoder(),
79+
/// as: .production
80+
/// )
81+
///
82+
/// var developmentConfig = productionConfig
83+
/// developmentConfig.environment = .sandbox
84+
///
85+
/// app.apns.containers.use(
86+
/// developmentConfig,
87+
/// eventLoopGroupProvider: .shared(app.eventLoopGroup),
88+
/// responseDecoder: JSONDecoder(),
89+
/// requestEncoder: JSONEncoder(),
90+
/// as: .development
91+
/// )
92+
/// ```
93+
///
94+
/// As shown above, the same key can be used for both the development and production environments.
95+
///
96+
/// - Important: Make sure not to store your APNs key within your code or repo directly, and opt to store it via a secure store specific to your deployment, such as in a .env supplied at deploy time.
97+
///
98+
/// You can determine which environment is being used in your app by checking its entitlements, and including the information along with the device token when sending it to your server:
99+
/// ```swift
100+
/// enum APNSDeviceTokenEnvironment: String {
101+
/// case production
102+
/// case development
103+
/// }
104+
///
105+
/// /// Get the APNs environment from the embedded
106+
/// /// provisioning profile, or nil if it can't
107+
/// /// be determined.
108+
/// ///
109+
/// /// Note that both TestFlight and the App Store
110+
/// /// don't have provisioning profiles, and always
111+
/// /// run in the production environment.
112+
/// var pushEnvironment: APNSDeviceTokenEnvironment? {
113+
/// #if canImport(AppKit)
114+
/// let provisioningProfileURL = Bundle.main.bundleURL
115+
/// .appending(path: "Contents", directoryHint: .isDirectory)
116+
/// .appending(path: "embedded.provisionprofile", directoryHint: .notDirectory)
117+
/// guard let data = try? Data(contentsOf: provisioningProfileURL)
118+
/// else { return nil }
119+
/// #else
120+
/// guard
121+
/// let provisioningProfileURL = Bundle.main
122+
/// .url(forResource: "embedded", withExtension: "mobileprovision"),
123+
/// let data = try? Data(contentsOf: provisioningProfileURL)
124+
/// else {
125+
/// #if targetEnvironment(simulator)
126+
/// return .development
127+
/// #else
128+
/// return nil
129+
/// #endif
130+
/// }
131+
/// #endif
132+
///
133+
/// let string = String(decoding: data, as: UTF8.self)
134+
///
135+
/// guard
136+
/// let start = string.firstRange(of: "<plist"),
137+
/// let end = string.firstRange(of: "</plist>")
138+
/// else { return nil }
139+
///
140+
/// let propertylist = string[start.lowerBound..<end.upperBound]
141+
///
142+
/// guard
143+
/// let provisioningProfile = try? PropertyListSerialization
144+
/// .propertyList(from: Data(propertylist.utf8), format: nil) as? [String : Any],
145+
/// let entitlements = provisioningProfile["Entitlements"] as? [String: Any],
146+
/// let environment = (
147+
/// entitlements["aps-environment"]
148+
/// ?? entitlements["com.apple.developer.aps-environment"]
149+
/// ) as? String
150+
/// else { return nil }
151+
///
152+
/// return APNSDeviceTokenEnvironment(rawValue: environment)
153+
/// }
154+
/// ```
155+
/// Note that the simulator doesn't have a provisioning profile, and will always register under the development environment.
156+
///
157+
/// - Parameters:
158+
/// - config: The APNs configuration.
159+
/// - eventLoopGroupProvider: Specify how the ``NIOCore/EventLoopGroup`` will be created. Example: `.shared(app.eventLoopGroup)`
160+
/// - responseDecoder: A decoder to use when decoding responses from the APNs server. Example: `JSONDecoder()`
161+
/// - requestEncoder: An encoder to use when encoding notifications. Example: `JSONEncoder()`
162+
/// - byteBufferAllocator: The allocator to use.
163+
/// - id: The container ID to access the configuration under.
164+
/// - isDefault: A flag to specify the configuration as the default when ``Vapor/Application/APNS/client`` is called. The first configuration that doesn't specify `false` is automatically configured as the default.
54165
public func use(
55166
_ config: APNSClientConfiguration,
56167
eventLoopGroupProvider: NIOEventLoopGroupProvider,

Sources/VaporAPNS/Application+APNS.swift

+62
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,65 @@ extension Application {
4747
}
4848
}
4949
}
50+
51+
extension Application.APNS {
52+
/// Configure both a production and development APNs environment.
53+
///
54+
/// This convenience method creates two clients available via ``client(_:)`` with ``APNSContainers/ID/production`` and ``APNSContainers/ID/development`` that make it easy to support both development builds (ie. run from Xcode) and release builds (ie. TestFlight/App Store):
55+
///
56+
/// ```swift
57+
/// /// The .p8 file as a string.
58+
/// guard let apnsKey = Environment.get("APNS_KEY_P8")
59+
/// else { throw Abort(.serviceUnavailable) }
60+
///
61+
/// app.apns.configure(.jwt(
62+
/// privateKey: try .loadFrom(string: apnsKey),
63+
/// /// The identifier of the key in the developer portal.
64+
/// keyIdentifier: Environment.get("APNS_KEY_ID"),
65+
/// /// The team identifier of the app in the developer portal.
66+
/// teamIdentifier: Environment.get("APNS_TEAM_ID")
67+
/// ))
68+
///
69+
/// // ...
70+
///
71+
/// let response = switch deviceToken.environment {
72+
/// case .production:
73+
/// try await apns.client(.production)
74+
/// .sendAlertNotification(notification, deviceToken: deviceToken.hexadecimalToken)
75+
/// case .development:
76+
/// try await apns.client(.development)
77+
/// .sendAlertNotification(notification, deviceToken: deviceToken.hexadecimalToken)
78+
/// }
79+
/// ```
80+
///
81+
/// For more control over configuration, including sample code to determine the environment an APFs device token belongs to, see ``APNSContainers/use(_:eventLoopGroupProvider:responseDecoder:requestEncoder:byteBufferAllocator:as:isDefault:)``.
82+
///
83+
/// - Note: The same key can be used for both the development and production environments.
84+
///
85+
/// - Important: Make sure not to store your APNs key within your code or repo directly, and opt to store it via a secure store specific to your deployment, such as in a .env supplied at deploy time.
86+
///
87+
/// - Parameter authenticationMethod: An APNs authentication method to use when connecting to Apple's production and development servers.
88+
public func configure(_ authenticationMethod: APNSClientConfiguration.AuthenticationMethod) {
89+
containers.use(
90+
APNSClientConfiguration(
91+
authenticationMethod: authenticationMethod,
92+
environment: .production
93+
),
94+
eventLoopGroupProvider: .shared(application.eventLoopGroup),
95+
responseDecoder: JSONDecoder(),
96+
requestEncoder: JSONEncoder(),
97+
as: .production
98+
)
99+
100+
containers.use(
101+
APNSClientConfiguration(
102+
authenticationMethod: authenticationMethod,
103+
environment: .sandbox
104+
),
105+
eventLoopGroupProvider: .shared(application.eventLoopGroup),
106+
responseDecoder: JSONDecoder(),
107+
requestEncoder: JSONEncoder(),
108+
as: .development
109+
)
110+
}
111+
}

0 commit comments

Comments
 (0)