Skip to content

Commit

Permalink
Container Identifiers (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
dimitribouniol authored Aug 20, 2024
1 parent d00f13b commit dedccc1
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 1 deletion.
20 changes: 20 additions & 0 deletions Sources/VaporAPNS/APNSContainerID.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@

extension APNSContainers.ID {
/// A default container ID available for use.
///
/// If you are configuring both a production and development container, ``production`` and ``development`` are also available.
///
/// - Note: You must configure this ID before using it by calling ``VaporAPNS/APNSContainers/use(_:eventLoopGroupProvider:responseDecoder:requestEncoder:byteBufferAllocator:as:isDefault:)``.
/// - 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:`.
public static var `default`: APNSContainers.ID {
return .init(string: "default")
}

/// An ID that can be used for the production APNs environment.
///
/// - Note: You must configure this ID before using it by calling ``APNSContainers/use(_:eventLoopGroupProvider:responseDecoder:requestEncoder:byteBufferAllocator:as:isDefault:)``
public static var production: APNSContainers.ID {
return .init(string: "production")
}

/// An ID that can be used for the development (aka sandbox) APNs environment.
///
/// - Note: You must configure this ID before using it by calling ``APNSContainers/use(_:eventLoopGroupProvider:responseDecoder:requestEncoder:byteBufferAllocator:as:isDefault:)``
public static var development: APNSContainers.ID {
return .init(string: "development")
}
}
113 changes: 112 additions & 1 deletion Sources/VaporAPNS/APNSContainers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,118 @@ public final class APNSContainers: Sendable {
}

extension APNSContainers {

/// Configure APNs for a given container ID.
///
/// 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:
///
/// ```swift
/// /// The .p8 file as a string.
/// let apnsKey = Environment.get("APNS_KEY_P8")
/// /// The identifier of the key in the developer portal.
/// let keyIdentifier = Environment.get("APNS_KEY_ID")
/// /// The team identifier of the app in the developer portal.
/// let teamIdentifier = Environment.get("APNS_TEAM_ID")
///
/// let productionConfig = APNSClientConfiguration(
/// authenticationMethod: .jwt(
/// privateKey: try .loadFrom(string: apnsKey),
/// keyIdentifier: keyIdentifier,
/// teamIdentifier: teamIdentifier
/// ),
/// environment: .production
/// )
///
/// app.apns.containers.use(
/// productionConfig,
/// eventLoopGroupProvider: .shared(app.eventLoopGroup),
/// responseDecoder: JSONDecoder(),
/// requestEncoder: JSONEncoder(),
/// as: .production
/// )
///
/// var developmentConfig = productionConfig
/// developmentConfig.environment = .sandbox
///
/// app.apns.containers.use(
/// developmentConfig,
/// eventLoopGroupProvider: .shared(app.eventLoopGroup),
/// responseDecoder: JSONDecoder(),
/// requestEncoder: JSONEncoder(),
/// as: .development
/// )
/// ```
///
/// As shown above, the same key can be used for both the development and production environments.
///
/// - 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.
///
/// 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:
/// ```swift
/// enum APNSDeviceTokenEnvironment: String {
/// case production
/// case development
/// }
///
/// /// Get the APNs environment from the embedded
/// /// provisioning profile, or nil if it can't
/// /// be determined.
/// ///
/// /// Note that both TestFlight and the App Store
/// /// don't have provisioning profiles, and always
/// /// run in the production environment.
/// var pushEnvironment: APNSDeviceTokenEnvironment? {
/// #if canImport(AppKit)
/// let provisioningProfileURL = Bundle.main.bundleURL
/// .appending(path: "Contents", directoryHint: .isDirectory)
/// .appending(path: "embedded.provisionprofile", directoryHint: .notDirectory)
/// guard let data = try? Data(contentsOf: provisioningProfileURL)
/// else { return nil }
/// #else
/// guard
/// let provisioningProfileURL = Bundle.main
/// .url(forResource: "embedded", withExtension: "mobileprovision"),
/// let data = try? Data(contentsOf: provisioningProfileURL)
/// else {
/// #if targetEnvironment(simulator)
/// return .development
/// #else
/// return nil
/// #endif
/// }
/// #endif
///
/// let string = String(decoding: data, as: UTF8.self)
///
/// guard
/// let start = string.firstRange(of: "<plist"),
/// let end = string.firstRange(of: "</plist>")
/// else { return nil }
///
/// let propertylist = string[start.lowerBound..<end.upperBound]
///
/// guard
/// let provisioningProfile = try? PropertyListSerialization
/// .propertyList(from: Data(propertylist.utf8), format: nil) as? [String : Any],
/// let entitlements = provisioningProfile["Entitlements"] as? [String: Any],
/// let environment = (
/// entitlements["aps-environment"]
/// ?? entitlements["com.apple.developer.aps-environment"]
/// ) as? String
/// else { return nil }
///
/// return APNSDeviceTokenEnvironment(rawValue: environment)
/// }
/// ```
/// Note that the simulator doesn't have a provisioning profile, and will always register under the development environment.
///
/// - Parameters:
/// - config: The APNs configuration.
/// - eventLoopGroupProvider: Specify how the ``NIOCore/EventLoopGroup`` will be created. Example: `.shared(app.eventLoopGroup)`
/// - responseDecoder: A decoder to use when decoding responses from the APNs server. Example: `JSONDecoder()`
/// - requestEncoder: An encoder to use when encoding notifications. Example: `JSONEncoder()`
/// - byteBufferAllocator: The allocator to use.
/// - id: The container ID to access the configuration under.
/// - 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.
public func use(
_ config: APNSClientConfiguration,
eventLoopGroupProvider: NIOEventLoopGroupProvider,
Expand Down
62 changes: 62 additions & 0 deletions Sources/VaporAPNS/Application+APNS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,65 @@ extension Application {
}
}
}

extension Application.APNS {
/// Configure both a production and development APNs environment.
///
/// 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):
///
/// ```swift
/// /// The .p8 file as a string.
/// guard let apnsKey = Environment.get("APNS_KEY_P8")
/// else { throw Abort(.serviceUnavailable) }
///
/// app.apns.configure(.jwt(
/// privateKey: try .loadFrom(string: apnsKey),
/// /// The identifier of the key in the developer portal.
/// keyIdentifier: Environment.get("APNS_KEY_ID"),
/// /// The team identifier of the app in the developer portal.
/// teamIdentifier: Environment.get("APNS_TEAM_ID")
/// ))
///
/// // ...
///
/// let response = switch deviceToken.environment {
/// case .production:
/// try await apns.client(.production)
/// .sendAlertNotification(notification, deviceToken: deviceToken.hexadecimalToken)
/// case .development:
/// try await apns.client(.development)
/// .sendAlertNotification(notification, deviceToken: deviceToken.hexadecimalToken)
/// }
/// ```
///
/// 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:)``.
///
/// - Note: The same key can be used for both the development and production environments.
///
/// - 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.
///
/// - Parameter authenticationMethod: An APNs authentication method to use when connecting to Apple's production and development servers.
public func configure(_ authenticationMethod: APNSClientConfiguration.AuthenticationMethod) {
containers.use(
APNSClientConfiguration(
authenticationMethod: authenticationMethod,
environment: .production
),
eventLoopGroupProvider: .shared(application.eventLoopGroup),
responseDecoder: JSONDecoder(),
requestEncoder: JSONEncoder(),
as: .production
)

containers.use(
APNSClientConfiguration(
authenticationMethod: authenticationMethod,
environment: .sandbox
),
eventLoopGroupProvider: .shared(application.eventLoopGroup),
responseDecoder: JSONDecoder(),
requestEncoder: JSONEncoder(),
as: .development
)
}
}

0 comments on commit dedccc1

Please sign in to comment.