Skip to content

Commit 61fa522

Browse files
committed
Update and refactor, support visionOS, add tests
1 parent 91a51ee commit 61fa522

26 files changed

+723
-630
lines changed

Sources/SuperwallKit/Dependencies/DependencyContainer.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ final class DependencyContainer {
4141
var webEntitlementRedeemer: WebEntitlementRedeemer!
4242
var deepLinkRouter: DeepLinkRouter!
4343
var attributionFetcher: AttributionFetcher!
44-
var userPermissions: UserPermissions!
44+
var permissionHandler: PermissionHandling!
4545
// swiftlint:enable implicitly_unwrapped_optional
4646
let paywallArchiveManager = PaywallArchiveManager()
4747

@@ -184,7 +184,7 @@ final class DependencyContainer {
184184
factory: self
185185
)
186186

187-
userPermissions = UserPermissionsImpl()
187+
permissionHandler = PermissionHandler()
188188
}
189189
}
190190

@@ -268,7 +268,7 @@ extension DependencyContainer: ViewControllerFactory {
268268
let messageHandler = PaywallMessageHandler(
269269
receiptManager: receiptManager,
270270
factory: self,
271-
userPermissions: userPermissions
271+
permissionHandler: permissionHandler
272272
)
273273
let webView = SWWebView(
274274
isMac: deviceHelper.isMac,

Sources/SuperwallKit/Graveyard/SuperwallGraveyard.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ extension Superwall {
9999
messageHandler: .init(
100100
receiptManager: dependencyContainer.receiptManager,
101101
factory: dependencyContainer,
102-
userPermissions: dependencyContainer.userPermissions
102+
permissionHandler: dependencyContainer.permissionHandler
103103
),
104104
isOnDeviceCacheEnabled: false,
105105
factory: dependencyContainer

Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift

Lines changed: 21 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ final class PaywallMessageHandler: WebEventDelegate {
3030
weak var delegate: PaywallMessageHandlerDelegate?
3131
private unowned let receiptManager: ReceiptManager
3232
private let factory: VariablesFactory
33-
private let userPermissions: UserPermissions
33+
private let permissionHandler: PermissionHandling
3434

3535
struct EnqueuedMessage {
3636
let name: String
@@ -42,11 +42,11 @@ final class PaywallMessageHandler: WebEventDelegate {
4242
init(
4343
receiptManager: ReceiptManager,
4444
factory: VariablesFactory,
45-
userPermissions: UserPermissions
45+
permissionHandler: PermissionHandling
4646
) {
4747
self.receiptManager = receiptManager
4848
self.factory = factory
49-
self.userPermissions = userPermissions
49+
self.permissionHandler = permissionHandler
5050
}
5151

5252
func handle(_ message: PaywallMessage) {
@@ -187,7 +187,11 @@ final class PaywallMessageHandler: WebEventDelegate {
187187
)
188188
delegate?.eventDidOccur(.scheduleNotification(notification: notification))
189189
case let .requestPermission(permissionType, requestId):
190-
handleRequestPermission(permissionType: permissionType, requestId: requestId)
190+
handleRequestPermission(
191+
permissionType: permissionType,
192+
requestId: requestId,
193+
paywall: paywall
194+
)
191195
}
192196
}
193197

@@ -489,64 +493,40 @@ final class PaywallMessageHandler: WebEventDelegate {
489493

490494
private func handleRequestPermission(
491495
permissionType: PermissionType,
492-
requestId: String
496+
requestId: String,
497+
paywall: Paywall
493498
) {
494-
let paywallIdentifier = delegate?.paywall.identifier ?? ""
495499
let permissionName = permissionType.rawValue
496500

497-
// Notify delegate that permission was requested
498-
delegate?.eventDidOccur(.requestPermission(permissionType: permissionType, requestId: requestId))
499-
500501
Task {
501502
// Track permission requested event
502503
let requestedEvent = InternalSuperwallEvent.Permission(
503504
state: .requested,
504505
permissionName: permissionName,
505-
paywallIdentifier: paywallIdentifier
506+
paywallIdentifier: paywall.identifier
506507
)
507508
await Superwall.shared.track(requestedEvent)
508509

509-
let status = await userPermissions.requestPermission(permissionType)
510+
let status = await permissionHandler.requestPermission(permissionType)
510511

511512
// Track permission result event
512513
let resultState: InternalSuperwallEvent.PermissionState = status == .granted ? .granted : .denied
513514
let resultEvent = InternalSuperwallEvent.Permission(
514515
state: resultState,
515516
permissionName: permissionName,
516-
paywallIdentifier: paywallIdentifier
517+
paywallIdentifier: paywall.identifier
517518
)
518519
await Superwall.shared.track(resultEvent)
519520

520-
await sendPermissionResult(
521-
requestId: requestId,
522-
permissionType: permissionType,
523-
status: status
524-
)
525-
}
526-
}
527-
528-
private func sendPermissionResult(
529-
requestId: String,
530-
permissionType: PermissionType,
531-
status: PermissionStatus
532-
) async {
533-
let event: [String: Any] = [
534-
"event_name": "permission_result",
535-
"permission_type": permissionType.rawValue,
536-
"request_id": requestId,
537-
"status": status.rawValue
538-
]
539-
540-
guard let jsonData = try? JSONSerialization.data(withJSONObject: [event]) else {
541-
Logger.debug(
542-
logLevel: .error,
543-
scope: .paywallViewController,
544-
message: "Failed to serialize permission result"
521+
await pass(
522+
placement: "permission_result",
523+
from: paywall,
524+
payload: [
525+
"permission_type": permissionType.rawValue,
526+
"request_id": requestId,
527+
"status": status.rawValue
528+
]
545529
)
546-
return
547530
}
548-
549-
let base64Event = jsonData.base64EncodedString()
550-
passMessageToWebView(base64Event)
551531
}
552532
}

Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallWebEvent.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,4 @@ enum PaywallWebEvent: Equatable {
1818
case customPlacement(name: String, params: JSON)
1919
case scheduleNotification(notification: LocalNotification)
2020
case userAttributesUpdated(attributes: JSON)
21-
case requestPermission(permissionType: PermissionType, requestId: String)
2221
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
// AuthorizationStatus+PermissionStatus.swift
3+
// SuperwallKit
4+
//
5+
// Created by Superwall on 2024.
6+
//
7+
8+
import AVFoundation
9+
import Contacts
10+
import CoreLocation
11+
import Photos
12+
import UserNotifications
13+
14+
extension UNAuthorizationStatus {
15+
var toPermissionStatus: PermissionStatus {
16+
switch self {
17+
case .authorized, .provisional, .ephemeral:
18+
return .granted
19+
case .denied, .notDetermined:
20+
return .denied
21+
@unknown default:
22+
return .unsupported
23+
}
24+
}
25+
}
26+
27+
extension CLAuthorizationStatus {
28+
var toPermissionStatus: PermissionStatus {
29+
switch self {
30+
case .authorizedWhenInUse, .authorizedAlways:
31+
return .granted
32+
case .denied, .restricted, .notDetermined:
33+
return .denied
34+
@unknown default:
35+
return .unsupported
36+
}
37+
}
38+
39+
var toBackgroundPermissionStatus: PermissionStatus {
40+
switch self {
41+
case .authorizedAlways:
42+
return .granted
43+
case .authorizedWhenInUse, .denied, .restricted, .notDetermined:
44+
return .denied
45+
@unknown default:
46+
return .unsupported
47+
}
48+
}
49+
}
50+
51+
extension PHAuthorizationStatus {
52+
var toPermissionStatus: PermissionStatus {
53+
switch self {
54+
case .authorized,
55+
.limited:
56+
return .granted
57+
case .denied,
58+
.restricted,
59+
.notDetermined:
60+
return .denied
61+
@unknown default:
62+
return .unsupported
63+
}
64+
}
65+
}
66+
67+
extension CNAuthorizationStatus {
68+
var toPermissionStatus: PermissionStatus {
69+
switch self {
70+
case .authorized,
71+
.limited:
72+
return .granted
73+
case .denied,
74+
.restricted,
75+
.notDetermined:
76+
return .denied
77+
@unknown default:
78+
// Handles .limited on iOS 18+ and future cases
79+
return .granted
80+
}
81+
}
82+
}
83+
84+
extension AVAuthorizationStatus {
85+
var toPermissionStatus: PermissionStatus {
86+
switch self {
87+
case .authorized:
88+
return .granted
89+
case .denied,
90+
.restricted,
91+
.notDetermined:
92+
return .denied
93+
@unknown default:
94+
return .unsupported
95+
}
96+
}
97+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// PermissionHandler+Camera.swift
3+
// SuperwallKit
4+
//
5+
// Created by Superwall on 2024.
6+
//
7+
8+
import AVFoundation
9+
10+
extension PermissionHandler {
11+
func checkCameraPermission() -> PermissionStatus {
12+
return AVCaptureDevice.authorizationStatus(for: .video).toPermissionStatus
13+
}
14+
15+
func requestCameraPermission() async -> PermissionStatus {
16+
guard hasPlistKey(PlistKey.camera) else {
17+
Logger.debug(
18+
logLevel: .error,
19+
scope: .paywallViewController,
20+
message: "Missing \(PlistKey.camera) in Info.plist. Cannot request camera permission."
21+
)
22+
return .unsupported
23+
}
24+
25+
let currentStatus = checkCameraPermission()
26+
if currentStatus == .granted {
27+
return .granted
28+
}
29+
30+
let granted = await AVCaptureDevice.requestAccess(for: .video)
31+
return granted ? .granted : .denied
32+
}
33+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// PermissionHandler+Contacts.swift
3+
// SuperwallKit
4+
//
5+
// Created by Superwall on 2024.
6+
//
7+
8+
import Contacts
9+
10+
extension PermissionHandler {
11+
func checkContactsPermission() -> PermissionStatus {
12+
return CNContactStore.authorizationStatus(for: .contacts).toPermissionStatus
13+
}
14+
15+
func requestContactsPermission() async -> PermissionStatus {
16+
guard hasPlistKey(PlistKey.contacts) else {
17+
Logger.debug(
18+
logLevel: .error,
19+
scope: .paywallViewController,
20+
message: "Missing \(PlistKey.contacts) in Info.plist. Cannot request contacts permission."
21+
)
22+
return .unsupported
23+
}
24+
25+
let currentStatus = checkContactsPermission()
26+
if currentStatus == .granted {
27+
return .granted
28+
}
29+
30+
let store = CNContactStore()
31+
do {
32+
let granted = try await store.requestAccess(for: .contacts)
33+
return granted ? .granted : .denied
34+
} catch {
35+
Logger.debug(
36+
logLevel: .error,
37+
scope: .paywallViewController,
38+
message: "Error requesting contacts permission",
39+
error: error
40+
)
41+
return .denied
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)