diff --git a/README.md b/README.md
index 5e6039c..33f1799 100755
--- a/README.md
+++ b/README.md
@@ -77,11 +77,16 @@ Because this library demands OIDC support from the Penn Labs Platform, a client
- `analyticsRoot: String`: The root keypath for analytics tokens. For example, in Penn Mobile, all analytics values will look like `pennmobile.{FEATURE}.{SUBFEATURE}`. In that case, the `analyticsRoot = "pennmobile"`.
- I decided to not make analytics optional. This is because I wanted the analytics requests to be simple with very little room for failure. If analytics were optional, I would want all analytics functions to throw errors instead of silently failing, which would lead to additional complexity when calling these functions.
- `clientId: String` and `redirectUrl: String`. These are issued by Platform.
+- `configuration: LabsPlatform.Configuration`. See below.
- `loginHandler: (Bool) async -> Void`: This function is run on startup and when the login state changes. The boolean argument is `true` when the user is logged in, `false` otherwise.
- `defaultLoginHandler: () -> Void`: The App Store requires a default login for most apps (for App Store verification purposes). This function will run if the default login credentials are intercepted by the login WebView.
+## Configuration
+Many features of LabsPlatformSwift can be configured. You can see all configuration options by looking at the `LabsPlatform.Configuration` object.
+
+Observe that **analytics can be globally disabled** by passing a `nil` `AnalyticsConfiguration` object to the `LabsPlatform.Configuration`. In this case, all analytics requests will silently fail/do nothing.
(back to top)
@@ -118,7 +123,7 @@ When the button is pressed, a WebView sheet should appear, prompting a log-in us
There are a few ways to approach network requests using this library. An important factor is the endpoint: Swift `URLRequest` often does not retain authorization headers if the network request is redirected. Hence, the package provides two ways of approaching authenticated network requests.
### Aside
-For both methods, the user has the option to choose between two `PlatformAuthMode`s: `.jwt` and `.accessToken`. These are used by the various Penn Labs services. In most cases, a developer may opt to use the `.accessToken` (since this token is supported by most Penn Labs Mobile Backend services). However, there are some services (like Analytics) that require a JWT (JSON Web Token).
+For both methods, the user has the option to choose between two `PlatformAuthMode`s: `.jwt` and `.accessToken`. These are used by the various Penn Labs services. In most cases, a developer may opt to use the `.accessToken` (since this token is supported by most Penn Labs Mobile Backend services, as well as analytics). However, we also provide the option to pass the ID Token JSON Web Token, if passing an ID token for some kind of validation is desired by one's backend server.
While Analytics, for example, is handled natively by this library, access to both kinds of tokens is given.
@@ -131,7 +136,7 @@ The package provides an extension to the `Foundation.URLRequest` class. You can
// replace with your URL
let url = URL(string: "https://platform.pennlabs.org/accounts/me/")!
-var request: URLRequest = try await URLRequest(url: url, mode: PlatformAuthMode.jwt)
+var request: URLRequest = try await URLRequest(url: url, mode: PlatformAuthMode.accessToken)
```
Note that this initializer is both asynchronous and throwing. It is asynchronous because it fetches a refreshed token prior to returning the `URLRequest` object. It is throwing because the user may not be logged in, Platform may not be enabled, or other issues may arise that prevent the creation of an authenticated `URLRequest`. Hence, another way of handling this is as follows (more code is provided, for reference, since this is the intended use)
@@ -139,7 +144,7 @@ Note that this initializer is both asynchronous and throwing. It is asynchronous
```swift
func getMyIdentity() async -> Identity? {
let url = URL(string: "https://platform.pennlabs.org/accounts/me/")!
- guard let request = try? await URLRequest(url: url, mode: PlatformAuthMode.jwt) else {
+ guard let request = try? await URLRequest(url: url, mode: PlatformAuthMode.accessToken) else {
return nil
}
@@ -162,7 +167,7 @@ The package provides an extension to the `Foundation.URLSession` class. You can
```swift
// This has an optional config parameter that defaults to URLSessionConfiguration.default, but can be overridden.
-var session: URLSession = try await URLSession(mode: .jwt)
+var session: URLSession = try await URLSession(mode: .accessToken)
```
@@ -173,7 +178,7 @@ func getMyIdentity() async -> Identity? {
// replace with your URL
let url = URL(string: "https://platform.pennlabs.org/accounts/me/")!
- guard let session = try? await URLSession(mode: .jwt) else {
+ guard let session = try? await URLSession(mode: .accessToken) else {
return nil
}
@@ -199,10 +204,16 @@ A large motivation for the project was to implement easy-to-use analytics into o
Analytics are incredibly valuable when making design or roadmap decisions. Given enough time and data, analytics allow developer to understand points of **friction** in their applications, perform **A/B testing** (not implemented...yet?), and otherwise better understand the **user experience** in a quantifiable way.
-*Side note: this library was originally designed solely for the brand new Penn Labs Analytics API, but upon realizing it requires JWT for verification, the package's objective was widened to support general authentication as well.*
+*Side note: this library was originally designed solely for the brand new Penn Labs Analytics API, but upon realizing it requires authentication with Platform directly, the package's objective was widened to support general authentication as well.*
The library was designed to make logging analytics simple, especially in SwiftUI-based View Hierarchies. However, there are other ways to log analytics that can be done in non SwiftUI-based contexts.
+### Configuration
+
+The Analytics configuration is part of the overall LabsPlatform object configuration. This means you can adjust properties such as the endpoint for analytics posts, the interval after which tokens are expired (removed from memory), or the period/interval in which these values are pushed to the endpoint.
+
+Analytics can be disabled by setting a `nil` configuration for the Analytics.Configuration object.
+
### SwiftUI Analytics Logging
Given an analytics key: `pennmobile.dining.kcech.breakfast.appear`, it is easy to understand the general structure. Different paths are separated by `.`, enabling an easy understanding of the exact hierarchy that led to the given key.
@@ -415,7 +426,7 @@ Consider the following usage, using our very own authenticated web requests.
func updateIdentity() {
Task.timedAnalyticsOperation(operation: "fetchIdentity") {
let url = URL(string: "https://platform.pennlabs.org/accounts/me/")!
- guard let request = try? await URLRequest(url: url, mode: PlatformAuthMode.jwt),
+ guard let request = try? await URLRequest(url: url, mode: PlatformAuthMode.accessToken),
let (data, response) = try? await URLSession.data(for: request),
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
@@ -428,15 +439,21 @@ func updateIdentity() {
By using `timedAnalyticsOperation`, the library creates an operation, runs the given task, then ends the operation and logs the total running time. This can be valuable in assessing loading times.
-The consideration is that because tasks do not take place in the view hierarchy, these operations will be labeled `global.operation.{NAME}`. Duplicate operation names have not been tested.
+The consideration is that because tasks do not take place in the view hierarchy, these operations will be labeled `global.operation.{NAME}`. **Duplicate operation names have not been adequately tested.**
### Analytics Wrap-Up
The analytics values we've worked hard to record are cached between app launches. This is to prevent unsent values from being lost if a user closes the app.
-As for regular use cases, the analytics values are kept in a queue, which is flushed, by default, every 30 seconds. This can be modified by changing the static property: `LabsPlatform.Analytics.pushInterval`. Note that this property should be changed before `enableLabsPlatform` is run, since initializing Platform starts the DispatchQueue on a set interval (which cannot then be changed).
+There are some properties in Analytics that can be configured when LabsPlatformSwift is initialized by providing an AnalyticsConfiguration object in the usual configuration.
+
+The first property that can be modified is the push URL. This allows for testing analytics using a local server or a server other than the production server.
+
+As for regular use cases, the analytics values are kept in a queue, which is flushed, by default, every 15 seconds. This can be modified by changing the static property: `pushInterval`. Note that this property should be changed before `enableLabsPlatform` is run, since initializing Platform starts the DispatchQueue on a set interval (which cannot then be changed).
+
+There is also a property called `bufferTime`, which defaults to 5 seconds. This buffer will not let analytics values within that time to be recorded. This is especially important for expensive geometric appearance calculation, where a user scrolling repeatedly would trigger many analytics values to be sent.
-Another property that can be changed is `LabsPlatform.Analytics.expireInterval`. By default, this value is set to `604800` seconds (7 days). On app launches, values created more than this interval ago are pruned from the queue. This is to prevent a backlog of analytics values in the event that there is a failure in some process.
+Another property that can be changed is `expireInterval`. By default, this value is set to `604800` seconds (7 days). On app launches, values created more than this interval ago are pruned from the queue. This is to prevent a backlog of analytics values in the event that there is a failure in some process.
Various endpoints can also be changed throughout the library.
diff --git a/Sources/Analytics/AnalyticsContext.swift b/Sources/Analytics/AnalyticsContext.swift
index 7d67fee..f088193 100755
--- a/Sources/Analytics/AnalyticsContext.swift
+++ b/Sources/Analytics/AnalyticsContext.swift
@@ -32,7 +32,7 @@ public struct AnalyticsContext {
guard let platform, let oper = await findOperation(key: key, operation: operation) else {
return
}
- await platform.analytics.completeTimedOperation(oper)
+ await platform.analytics?.completeTimedOperation(oper)
}
}
@@ -44,7 +44,7 @@ public struct AnalyticsContext {
subpath.append(path[j])
}
let trialPath = subpath.joined(separator: ".")
- if let oper = await platform?.analytics.getTimedOperation("\(trialPath).operation.\(operation)") {
+ if let oper = await platform?.analytics?.getTimedOperation("\(trialPath).operation.\(operation)") {
return oper
}
}
diff --git a/Sources/Analytics/AnalyticsContextProvider.swift b/Sources/Analytics/AnalyticsContextProvider.swift
index 1dd1b63..faa5d42 100755
--- a/Sources/Analytics/AnalyticsContextProvider.swift
+++ b/Sources/Analytics/AnalyticsContextProvider.swift
@@ -43,7 +43,14 @@ public struct AnalyticsContextProvider: View {
}
extension View {
+ @available(*, deprecated, message: "Switch to analytics(_:logViewAppearances:)")
@ViewBuilder public func analytics(_ subkey: String?, logViewAppearances: Bool) -> some View {
+ AnalyticsView(subkey: subkey, logViewAppearances: logViewAppearances ? .enabled : .disabled) {
+ self
+ }
+ }
+
+ @ViewBuilder public func analytics(_ subkey: String?, logViewAppearances: ViewAppearanceLoggingMode) -> some View {
AnalyticsView(subkey: subkey, logViewAppearances: logViewAppearances) {
self
}
@@ -53,11 +60,12 @@ extension View {
private struct AnalyticsView: View {
@Environment(\.labsAnalyticsPath) var path: String
let content: Content
- let logViewAppearances: Bool
+ let logViewAppearances: ViewAppearanceLoggingMode
let subkey: String?
@State var key: String = ""
+ @State var onScreen: Bool = false
let platform = LabsPlatform.shared
- init(subkey: String?, logViewAppearances: Bool, @ViewBuilder _ content: () -> Content) {
+ init(subkey: String?, logViewAppearances: ViewAppearanceLoggingMode, @ViewBuilder _ content: () -> Content) {
self.content = content()
self.subkey = subkey
self.logViewAppearances = logViewAppearances
@@ -65,18 +73,26 @@ private struct AnalyticsView: View {
var body: some View {
Group {
- if logViewAppearances {
+ if case .enabled = logViewAppearances {
content
.onAppear {
- guard let platform else { return }
- Task {
- await platform.analytics.record(AnalyticsValue(key: "\(key).appear", value: "1", timestamp: Date.now))
- }
+ onScreen = true
}
.onDisappear {
- guard let platform else { return }
- Task {
- await platform.analytics.record(AnalyticsValue(key: "\(key).disappear", value: "1", timestamp: Date.now))
+ onScreen = false
+ }
+ } else if case .enabledExpensive = logViewAppearances {
+ content
+ .background {
+ GeometryReader { proxy in
+ Color.clear
+ .onChange(of: proxy.frame(in: .global)) {
+ let frame = proxy.frame(in: .global)
+ let intersects = frame.intersects(.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
+ if intersects != onScreen {
+ onScreen = intersects
+ }
+ }
}
}
} else {
@@ -87,5 +103,16 @@ private struct AnalyticsView: View {
.onAppear {
self.key = subkey == nil ? path : "\(path).\(subkey!)"
}
+ .onChange(of: onScreen) {
+ Task {
+ await platform?.analytics?.record(AnalyticsValue(key: "\(key).\(onScreen ? "appear" : "disappear")", value: "1", timestamp: Date.now))
+ }
+ }
}
}
+
+public enum ViewAppearanceLoggingMode {
+ case disabled, enabled, enabledExpensive
+
+ // enabledExpensive for more accurate processing, such as for portal posts where we actually want to be sure that it was on screen, instead of just in the VStack
+}
diff --git a/Sources/Analytics/AnalyticsTimedOperation.swift b/Sources/Analytics/AnalyticsTimedOperation.swift
index 49e07c1..fe42d50 100755
--- a/Sources/Analytics/AnalyticsTimedOperation.swift
+++ b/Sources/Analytics/AnalyticsTimedOperation.swift
@@ -13,9 +13,9 @@ public extension Task where Success == Void, Failure == Never {
static func timedAnalyticsOperation(name: String, cancelOnScenePhase: [ScenePhase] = [.background, .inactive], _ operation: @Sendable @escaping () async -> Void) {
Task {
let analytic = AnalyticsTimedOperation(fullKey: "global.operation.\(name)", cancelOnScenePhase: cancelOnScenePhase)
- await LabsPlatform.shared?.analytics.addTimedOperation(analytic)
+ await LabsPlatform.shared?.analytics?.addTimedOperation(analytic)
await operation()
- await LabsPlatform.shared?.analytics.completeTimedOperation(analytic)
+ await LabsPlatform.shared?.analytics?.completeTimedOperation(analytic)
}
}
}
diff --git a/Sources/Auth/LabsPlatformAuth.swift b/Sources/Auth/LabsPlatformAuth.swift
index 33bf7bf..16817f0 100755
--- a/Sources/Auth/LabsPlatformAuth.swift
+++ b/Sources/Auth/LabsPlatformAuth.swift
@@ -82,7 +82,7 @@ extension LabsPlatform {
let verifier: String = AuthUtilities.codeVerifier()
let state: String = AuthUtilities.stateString()
guard let url = URL(string:
- "\(LabsPlatform.authEndpoint.absoluteString)?response_type=code&code_challenge=\(AuthUtilities.codeChallenge(from: verifier))&code_challenge_method=S256&client_id=\(self.clientId)&redirect_uri=\(self.authRedirect)&scope=openid%20read%20introspection&state=\(state)") else { throw PlatformAuthError.invalidUrl }
+ "\(configuration.authEndpoint.absoluteString)?response_type=code&code_challenge=\(AuthUtilities.codeChallenge(from: verifier))&code_challenge_method=S256&client_id=\(self.clientId)&redirect_uri=\(self.authRedirect)&scope=openid%20read%20introspection&state=\(state)") else { throw PlatformAuthError.invalidUrl }
return .newLogin(url: url, state: state, verifier: verifier)
}
@@ -241,7 +241,7 @@ extension LabsPlatform {
let postData = postString.data(using: .utf8)
- var request = URLRequest(url: LabsPlatform.tokenEndpoint)
+ var request = URLRequest(url: configuration.tokenEndpoint)
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = postData
@@ -271,7 +271,7 @@ extension LabsPlatform {
let postData = postString.data(using: .utf8)
- var request = URLRequest(url: LabsPlatform.tokenEndpoint)
+ var request = URLRequest(url: configuration.tokenEndpoint)
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = postData
@@ -291,7 +291,20 @@ extension LabsPlatform {
return .failure(DecodingError.valueNotFound(PlatformAuthCredentials.self, DecodingError.Context(codingPath: [], debugDescription: "Could not decode credentials")))
}
- return .success(data)
+ if let idToken = data.idToken {
+ return .success(data)
+ } else {
+ // specifically retain the ID token from initial auth (if available)
+
+ let combinedToken = PlatformAuthCredentials(
+ accessToken: data.accessToken,
+ expiresIn: data.expiresIn,
+ tokenType: data.tokenType,
+ refreshToken: data.refreshToken,
+ idToken: auth.idToken,
+ issuedAt: data.issuedAt)
+ return .success(combinedToken)
+ }
}
}
diff --git a/Sources/LabsAnalytics.swift b/Sources/LabsAnalytics.swift
index 24a5f04..981b9e2 100755
--- a/Sources/LabsAnalytics.swift
+++ b/Sources/LabsAnalytics.swift
@@ -11,9 +11,22 @@ import SwiftUI
public extension LabsPlatform {
final actor Analytics: ObservableObject, Sendable {
- static let defaultEndpoint: URL = URL(string: "https://analytics.pennlabs.org/analytics/")!
- static let defaultPushInterval: TimeInterval = 30
- static let defaultExpireInterval: TimeInterval = TimeInterval(60 * 60 * 24 * 7) // 7 days expiry
+ public struct Configuration {
+ let endpoint: URL
+ let pushInterval: TimeInterval
+ let expireInterval: TimeInterval
+ let bufferInterval: TimeInterval
+
+ public init(endpoint: URL = URL(string: "https://analytics.pennlabs.org/analytics/")!,
+ pushInterval: TimeInterval = 30,
+ expireInterval: TimeInterval = TimeInterval(60 * 60 * 24 * 7),
+ bufferInterval: TimeInterval = 5) {
+ self.endpoint = endpoint
+ self.pushInterval = pushInterval
+ self.expireInterval = expireInterval
+ self.bufferInterval = bufferInterval
+ }
+ }
private var queue: Set = [] {
didSet {
@@ -26,19 +39,17 @@ public extension LabsPlatform {
private var activeOperations: [AnalyticsTimedOperation] = []
private var dispatch: (any Cancellable)?
- let endpoint: URL
- let pushInterval: TimeInterval
- let expireInterval: TimeInterval
+ let configuration: Analytics.Configuration
- init(endpoint: URL = defaultEndpoint, pushInterval: TimeInterval = defaultPushInterval, expireInterval: TimeInterval = defaultExpireInterval) {
+ init?(configuration: Analytics.Configuration? = Analytics.Configuration()) {
+ guard let configuration else { return nil }
+
// queue will be assigned the value in userdefaults on the first submission, so we will expire old values
let data = UserDefaults.standard.data(forKey: "LabsAnalyticsQueue")
let oldQueue = (try? JSONDecoder().decode(Set.self, from: data ?? Data())) ?? []
- self.endpoint = endpoint
- self.pushInterval = pushInterval
- self.expireInterval = expireInterval
+ self.configuration = configuration
self.queue = oldQueue.filter {
- return Date.now.timeIntervalSince(Date.init(timeIntervalSince1970: TimeInterval($0.timestamp))) < expireInterval
+ return Date.now.timeIntervalSince(Date.init(timeIntervalSince1970: TimeInterval($0.timestamp))) < configuration.expireInterval
}
Task {
@@ -51,8 +62,8 @@ public extension LabsPlatform {
dispatch = DispatchQueue
.global(qos: .utility)
.schedule(after: .init(.now()),
- interval: .seconds(self.pushInterval),
- tolerance: .seconds(self.pushInterval / 5)) { [weak self] in
+ interval: .seconds(self.configuration.pushInterval),
+ tolerance: .seconds(self.configuration.pushInterval / 5)) { [weak self] in
guard let self else { return }
Task {
await self.submitQueue()
@@ -68,7 +79,16 @@ public extension LabsPlatform {
let pennkey: String = jwt["pennkey"] as? String else {
return
}
- self.queue.insert(AnalyticsTxn(pennkey: pennkey, timestamp: Date.now, data: [value]))
+
+ let now = Date.now
+
+ if let latest = self.queue.sorted(by: { $0.timestamp > $1.timestamp }).first(where: { $0.data.contains(where: { $0.key == value.key }) }),
+ Date.now.timeIntervalSince1970 - Double(latest.timestamp) < self.configuration.bufferInterval {
+ // we already have a token within buffer, though this is a really expensive operation
+ return
+ }
+
+ self.queue.insert(AnalyticsTxn(pennkey: pennkey, timestamp: now, data: [value]))
}
func recordAndSubmit(_ value: AnalyticsValue) async throws {
@@ -76,7 +96,7 @@ public extension LabsPlatform {
await submitQueue()
}
- func addTimedOperation(_ operation: AnalyticsTimedOperation) {
+ func addTimedOperation(_ operation: AnalyticsTimedOperation, removeOnDuplicateName: Bool = true) {
self.activeOperations.append(operation)
}
@@ -135,7 +155,7 @@ extension LabsPlatform.Analytics {
// though the analytics engine supports anonymous submissions
// (from logged in users from some reason)
private func analyticsPostRequest(_ txn: AnalyticsTxn) async -> Bool {
- guard var request = try? await URLRequest(url: self.endpoint, mode: .jwt) else {
+ guard var request = try? await URLRequest(url: self.configuration.endpoint, mode: .accessToken) else {
return false
}
request.httpMethod = "POST"
@@ -149,9 +169,8 @@ extension LabsPlatform.Analytics {
}
request.httpBody = data
-
- guard let (data, response) = try? await URLSession.shared.data(for: request),
- let httpResponse = response as? HTTPURLResponse,
+
+ guard let (data, response) = try? await URLSession.shared.data(for: request), let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
return false
}
diff --git a/Sources/LabsPlatform.swift b/Sources/LabsPlatform.swift
index 35e57e3..f90cbd2 100755
--- a/Sources/LabsPlatform.swift
+++ b/Sources/LabsPlatform.swift
@@ -11,14 +11,30 @@ import UIKit
@MainActor
public final class LabsPlatform: ObservableObject {
- public static var authEndpoint = URL(string: "https://platform.pennlabs.org/accounts/authorize")!
- public static var tokenEndpoint = URL(string: "https://platform.pennlabs.org/accounts/token/")!
- public static var defaultAccount = "root"
- public static var defaultPassword = "root"
+ public struct Configuration {
+ let authEndpoint: URL
+ let tokenEndpoint: URL
+ let defaultAccount: String
+ let defaultPassword: String
+
+ let analyticsConfiguration: Analytics.Configuration?
+
+ public init(authEndpoint: URL = URL(string: "https://platform.pennlabs.org/accounts/authorize")!,
+ tokenEndpoint: URL = URL(string: "https://platform.pennlabs.org/accounts/token/")!,
+ defaultAccount: String = "root",
+ defaultPassword: String = "root",
+ analyticsConfiguration: Analytics.Configuration? = Analytics.Configuration()) {
+ self.authEndpoint = authEndpoint
+ self.tokenEndpoint = tokenEndpoint
+ self.defaultAccount = defaultAccount
+ self.defaultPassword = defaultPassword
+ self.analyticsConfiguration = analyticsConfiguration
+ }
+ }
public private(set) static var shared: LabsPlatform?
- @Published var analytics: Analytics
+ @Published var analytics: Analytics?
@Published var webViewUrl: URL?
@Published var authState: PlatformAuthState = .idle
@Published var alertText: String? = nil
@@ -28,13 +44,18 @@ public final class LabsPlatform: ObservableObject {
let authRedirect: String
var webViewCheckedContinuation: CheckedContinuation?
+ let configuration: Configuration
+
- public init(clientId: String, redirectUrl: String) {
+ public init(clientId: String, redirectUrl: String, configuration: Configuration = Configuration()) {
self.clientId = clientId
self.authRedirect = redirectUrl
- self.analytics = Analytics()
+ self.configuration = configuration
self.authState = getCurrentAuthState()
LabsPlatform.shared = self
+ Task {
+ self.analytics = Analytics(configuration: configuration.analyticsConfiguration)
+ }
UserDefaults.standard.loadPlatformHTTPCookies()
}
@@ -56,8 +77,8 @@ struct PlatformProvider: View {
let loginHandler: (Bool) async -> ()
let defaultLoginHandler: (() -> ())?
- init(analyticsRoot: String, clientId: String, redirectUrl: String, loginHandler: @escaping (Bool) async -> (), defaultLoginHandler: (() -> ())? = nil, @ViewBuilder content: @escaping () -> Content) {
- self._platform = StateObject(wrappedValue: LabsPlatform(clientId: clientId, redirectUrl: redirectUrl))
+ init(analyticsRoot: String, clientId: String, redirectUrl: String, configuration: LabsPlatform.Configuration, loginHandler: @escaping (Bool) async -> (), defaultLoginHandler: (() -> ())? = nil, @ViewBuilder content: @escaping () -> Content) {
+ self._platform = StateObject(wrappedValue: LabsPlatform(clientId: clientId, redirectUrl: redirectUrl, configuration: configuration))
self.analyticsRoot = analyticsRoot
self.content = content()
self.loginHandler = loginHandler
@@ -121,7 +142,7 @@ struct PlatformProvider: View {
.onChange(of: scenePhase) { _ in
DispatchQueue.main.async {
Task {
- await platform.analytics.focusChanged(scenePhase)
+ await platform.analytics?.focusChanged(scenePhase)
}
}
}
@@ -188,14 +209,15 @@ public extension View {
/// - Parameters:
/// - clientId: A Platform-granted clientId that has permission to get JWTs
/// - redirectUrl: A valid redirect URI (allowed by the Platform application)
+ /// - configuration: An overridden configuration object (for when specific behavior modification is desired)
/// - defaultLoginHandler: A function that should be called when the login flow intercepts the default login credentials (user and password both "root", by default)
/// - loginHandler(loggedIn: Bool): a function that will be called whenever the Platform goes to either the logged-in state or the logged-out state. This includes
/// uses of the [`LabsPlatform.logoutPlatform()`](x-source-tag://logoutPlatform) function (will always be `false`)
///
/// - Returns: The original view with a `LabsPlatform.Analytics` environment object. The `LabsPlatform` instance can be accessed as a singleton: `LabsPlatform.shared`, though this is not recommended except for cases when logging in or out.
/// - Tag: enableLabsPlatform
- @ViewBuilder func enableLabsPlatform(analyticsRoot: String, clientId: String, redirectUrl: String, defaultLoginHandler: (() -> ())? = nil, _ loginHandler: @escaping (Bool) async -> ()) -> some View {
- PlatformProvider(analyticsRoot: analyticsRoot, clientId: clientId, redirectUrl: redirectUrl, loginHandler: loginHandler, defaultLoginHandler: defaultLoginHandler) {
+ @ViewBuilder func enableLabsPlatform(analyticsRoot: String, clientId: String, redirectUrl: String, configuration: LabsPlatform.Configuration = LabsPlatform.Configuration(), defaultLoginHandler: (() -> ())? = nil, _ loginHandler: @escaping (Bool) async -> ()) -> some View {
+ PlatformProvider(analyticsRoot: analyticsRoot, clientId: clientId, redirectUrl: redirectUrl, configuration: configuration, loginHandler: loginHandler, defaultLoginHandler: defaultLoginHandler) {
self
}
}