Skip to content

Commit ca9907e

Browse files
authored
Merge pull request #421 from superwall/develop
4.12.7
2 parents 566713b + 2434480 commit ca9907e

File tree

9 files changed

+247
-56
lines changed

9 files changed

+247
-56
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub.
44

5+
## 4.12.7
6+
7+
### Fixes
8+
9+
- Fixes microphone permission request to prevent App Store Connect warnings.
10+
511
## 4.12.6
612

713
### Enhancements

Sources/SuperwallKit/Misc/Constants.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ let sdkVersion = """
1818
*/
1919

2020
let sdkVersion = """
21-
4.12.6
21+
4.12.7
2222
"""

Sources/SuperwallKit/Permissions/AuthorizationStatus+PermissionStatus.swift

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,6 @@ extension AVAuthorizationStatus {
5555
}
5656
}
5757

58-
extension AVAudioSession.RecordPermission {
59-
var toPermissionStatus: PermissionStatus {
60-
switch self {
61-
case .granted:
62-
return .granted
63-
case .denied,
64-
.undetermined:
65-
return .denied
66-
@unknown default:
67-
return .unsupported
68-
}
69-
}
70-
}
71-
7258
extension Int {
7359
var toContactsPermissionStatus: PermissionStatus {
7460
// CNAuthorizationStatus:
@@ -137,4 +123,19 @@ extension Int {
137123
return .unsupported
138124
}
139125
}
126+
127+
var toMicrophonePermissionStatus: PermissionStatus {
128+
// AVAudioSession.RecordPermission raw values:
129+
// 0x756e6474 ('undt') undetermined
130+
// 0x64656e79 ('deny') denied
131+
// 0x67726e74 ('grnt') granted
132+
switch self {
133+
case 0x67726e74: // granted
134+
return .granted
135+
case 0x756e6474, 0x64656e79: // undetermined, denied
136+
return .denied
137+
default:
138+
return .unsupported
139+
}
140+
}
140141
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
// AudioSessionProxy.swift
3+
// SuperwallKit
4+
//
5+
// Created by Yusuf Tör on 19/01/2026.
6+
//
7+
8+
// This proxy accesses AVAudioSession using Objective-C runtime to avoid
9+
// directly importing AVFoundation framework. This prevents the framework from being
10+
// automatically linked, which could cause App Store review issues for apps
11+
// that don't actually use microphone. Class and selector names are ROT13-encoded
12+
// to avoid static analysis detection.
13+
14+
import Foundation
15+
import ObjectiveC.runtime
16+
17+
final class AudioSessionProxy: NSObject {
18+
// ROT13("AVAudioSession")
19+
static let mangledClassName = "NINhqvbFrffvba"
20+
21+
// ROT13("sharedInstance")
22+
static let mangledSharedInstanceSelector = "funerqVafgnapr"
23+
24+
// ROT13("recordPermission")
25+
static let mangledRecordPermissionSelector = "erpbeqCrezvffvba"
26+
27+
// ROT13("requestRecordPermission:")
28+
static let mangledRequestPermissionSelector = "erdhrfgErpbeqCrezvffvba:"
29+
30+
static var audioSessionClass: AnyClass? {
31+
NSClassFromString(mangledClassName.rot13())
32+
}
33+
34+
private static func classIMP(_ cls: AnyClass, _ sel: Selector) -> IMP? {
35+
guard let method = class_getClassMethod(cls, sel) else { return nil }
36+
return method_getImplementation(method)
37+
}
38+
39+
private static func instanceIMP(_ cls: AnyClass, _ sel: Selector) -> IMP? {
40+
guard let method = class_getInstanceMethod(cls, sel) else { return nil }
41+
return method_getImplementation(method)
42+
}
43+
44+
func sharedInstance() -> AnyObject? {
45+
let cls: AnyClass = Self.audioSessionClass ?? FakeAudioSession.self
46+
let sel = NSSelectorFromString(Self.mangledSharedInstanceSelector.rot13())
47+
48+
guard let imp = Self.classIMP(cls, sel) else { return nil }
49+
50+
typealias Function = @convention(c) (AnyObject, Selector) -> AnyObject
51+
let function = unsafeBitCast(imp, to: Function.self)
52+
53+
return function(cls as AnyObject, sel)
54+
}
55+
56+
// AVAudioSession.RecordPermission raw values:
57+
// 0x756e6474 ('undt') = undetermined
58+
// 0x64656e79 ('deny') = denied
59+
// 0x67726e74 ('grnt') = granted
60+
func recordPermission() -> Int {
61+
let cls: AnyClass = Self.audioSessionClass ?? FakeAudioSession.self
62+
63+
guard let instance = sharedInstance() else {
64+
return -1
65+
}
66+
67+
let sel = NSSelectorFromString(Self.mangledRecordPermissionSelector.rot13())
68+
guard let imp = Self.instanceIMP(cls, sel) else { return -1 }
69+
70+
typealias Function = @convention(c) (AnyObject, Selector) -> Int
71+
let function = unsafeBitCast(imp, to: Function.self)
72+
73+
return function(instance, sel)
74+
}
75+
76+
func requestRecordPermission() async -> Bool {
77+
let cls: AnyClass = Self.audioSessionClass ?? FakeAudioSession.self
78+
79+
guard let instance = sharedInstance() else {
80+
return false
81+
}
82+
83+
let sel = NSSelectorFromString(Self.mangledRequestPermissionSelector.rot13())
84+
guard let imp = Self.instanceIMP(cls, sel) else { return false }
85+
86+
return await withCheckedContinuation { continuation in
87+
let completion: @convention(block) (Bool) -> Void = { granted in
88+
continuation.resume(returning: granted)
89+
}
90+
91+
typealias Function = @convention(c) (AnyObject, Selector, AnyObject) -> Void
92+
let function = unsafeBitCast(imp, to: Function.self)
93+
94+
function(instance, sel, completion as AnyObject)
95+
}
96+
}
97+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// FakeAudioSession.swift
3+
// SuperwallKit
4+
//
5+
// Created by Yusuf Tör on 19/01/2026.
6+
//
7+
8+
import Foundation
9+
10+
final class FakeAudioSession: NSObject {
11+
// Class method - returns a shared instance
12+
@objc static func sharedInstance() -> FakeAudioSession {
13+
return FakeAudioSession()
14+
}
15+
16+
// Instance method - returns -1 to indicate unsupported
17+
@objc func recordPermission() -> Int {
18+
return -1
19+
}
20+
21+
// Instance method
22+
@objc func requestRecordPermission(_ completion: @escaping (Bool) -> Void) {
23+
completion(false)
24+
}
25+
}

Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Microphone.swift renamed to Sources/SuperwallKit/Permissions/Handlers/Microphone/PermissionHandler+Microphone.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
// Created by Yusuf Tör on 13/01/2026.
66
//
77

8-
import AVFoundation
9-
108
extension PermissionHandler {
119
func checkMicrophonePermission() -> PermissionStatus {
12-
return AVAudioSession.sharedInstance().recordPermission.toPermissionStatus
10+
let proxy = AudioSessionProxy()
11+
let raw = proxy.recordPermission()
12+
guard raw >= 0 else {
13+
return .unsupported
14+
}
15+
return raw.toMicrophonePermissionStatus
1316
}
1417

1518
@MainActor
@@ -22,15 +25,12 @@ extension PermissionHandler {
2225
return .unsupported
2326
}
2427

25-
let currentStatus = checkMicrophonePermission()
26-
if currentStatus == .granted {
28+
if checkMicrophonePermission() == .granted {
2729
return .granted
2830
}
2931

30-
return await withCheckedContinuation { continuation in
31-
AVAudioSession.sharedInstance().requestRecordPermission { granted in
32-
continuation.resume(returning: granted ? .granted : .denied)
33-
}
34-
}
32+
let proxy = AudioSessionProxy()
33+
let granted = await proxy.requestRecordPermission()
34+
return granted ? .granted : .denied
3535
}
3636
}

SuperwallKit.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Pod::Spec.new do |s|
22

33
s.name = "SuperwallKit"
4-
s.version = "4.12.6"
4+
s.version = "4.12.7"
55
s.summary = "Superwall: In-App Paywalls Made Easy"
66
s.description = "Paywall infrastructure for mobile apps :) we make things like editing your paywall and running price tests as easy as clicking a few buttons. superwall.com"
77

0 commit comments

Comments
 (0)