Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup

## 4.12.10

### Enhancements

- Adds native haptic feedback support for paywall buttons. Haptic types can be configured in the paywall editor and include light, medium, heavy, success, warning, error, and selection.

### Fixes

- Fixes issue where the `app_install` event was being cleared upon reset, which meant that this couldn't be used with `device.daysSince_app_install` after reset.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ enum PaywallMessage: Decodable, Equatable {
case initiateWebCheckout(contextId: String)
case requestStoreReview(ReviewType)
case requestPermission(permissionType: PermissionType, requestId: String)
case hapticFeedback(hapticType: String)

// All cases below here are sent from device to paywall
case paywallClose
Expand Down Expand Up @@ -98,6 +99,7 @@ enum PaywallMessage: Decodable, Equatable {
case requestStoreReview = "request_store_review"
case scheduleNotification = "schedule_notification"
case requestPermission = "request_permission"
case hapticFeedback = "haptic_feedback"
}

// Everyone write to eventName, other may use the remaining keys
Expand All @@ -123,6 +125,7 @@ enum PaywallMessage: Decodable, Equatable {
case delay
case permissionType
case requestId
case hapticType
}

enum PaywallMessageError: Error {
Expand Down Expand Up @@ -221,6 +224,11 @@ enum PaywallMessage: Decodable, Equatable {
self = .requestPermission(permissionType: permissionType, requestId: requestId)
return
}
case .hapticFeedback:
if let hapticType = try? values.decode(String.self, forKey: .hapticType) {
self = .hapticFeedback(hapticType: hapticType)
return
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@
requestId: requestId,
paywall: paywall
)
case let .hapticFeedback(hapticType):
triggerHapticFeedback(hapticType)
}
}

Expand Down Expand Up @@ -352,7 +354,7 @@
// block selection
let selectionString =
// swiftlint:disable:next line_length
"var css = '*{-webkit-touch-callout:none;-webkit-user-select:none} .w-webflow-badge { display: none !important; }'; "

Check warning on line 357 in Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Superfluous Disable Command Violation: SwiftLint rule 'line_length' did not trigger a violation in the disabled region; remove the disable command (superfluous_disable_command)
+ "var head = document.head || document.getElementsByTagName('head')[0]; "
+ "var style = document.createElement('style'); style.type = 'text/css'; "
+ "style.appendChild(document.createTextNode(css)); head.appendChild(style); "
Expand Down Expand Up @@ -508,6 +510,44 @@
#endif
}

/// Triggers haptic feedback based on the type specified from the paywall editor.
private func triggerHapticFeedback(_ hapticType: String) {
#if !os(visionOS)
switch hapticType {
case "light":
let generator = UIImpactFeedbackGenerator(style: .light)
generator.prepare()
generator.impactOccurred()
case "medium":
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.prepare()
generator.impactOccurred()
case "heavy":
let generator = UIImpactFeedbackGenerator(style: .heavy)
generator.prepare()
generator.impactOccurred()
case "success":
let generator = UINotificationFeedbackGenerator()
generator.prepare()
generator.notificationOccurred(.success)
case "warning":
let generator = UINotificationFeedbackGenerator()
generator.prepare()
generator.notificationOccurred(.warning)
case "error":
let generator = UINotificationFeedbackGenerator()
generator.prepare()
generator.notificationOccurred(.error)
case "selection":
let generator = UISelectionFeedbackGenerator()
generator.prepare()
generator.selectionChanged()
default:
break
}
#endif
}

// MARK: - Permission Handling

private func handleRequestPermission(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,4 +468,81 @@ struct PaywallMessageHandlerTests {
shouldDismiss: true
))
}

// MARK: - Haptic Feedback Message Decoding Tests

@Test
func decodeHapticFeedback_medium() throws {
let json = """
{
"version": 1,
"payload": {
"events": [
{
"event_name": "haptic_feedback",
"haptic_type": "medium"
}
]
}
}
"""
let data = json.data(using: .utf8)!
let wrapped = try JSONDecoder.fromSnakeCase.decode(WrappedPaywallMessages.self, from: data)
let message = wrapped.payload.messages.first

#expect(message == .hapticFeedback(hapticType: "medium"))
}

@Test
func decodeHapticFeedback_allTypes() throws {
let hapticTypes = ["light", "medium", "heavy", "success", "warning", "error", "selection"]

for hapticType in hapticTypes {
let json = """
{
"version": 1,
"payload": {
"events": [
{
"event_name": "haptic_feedback",
"haptic_type": "\(hapticType)"
}
]
}
}
"""
let data = json.data(using: .utf8)!
let wrapped = try JSONDecoder.fromSnakeCase.decode(WrappedPaywallMessages.self, from: data)
let message = wrapped.payload.messages.first

#expect(message == .hapticFeedback(hapticType: hapticType))
}
}

@Test
func handleHapticFeedback() {
let dependencyContainer = DependencyContainer()
let messageHandler = PaywallMessageHandler(
receiptManager: dependencyContainer.receiptManager,
factory: dependencyContainer,
permissionHandler: FakePermissionHandler()
)
let webView = FakeWebView(
isMac: false,
messageHandler: messageHandler,
isOnDeviceCacheEnabled: true,
factory: dependencyContainer
)
let delegate = PaywallMessageHandlerDelegateMock(
paywallInfo: .stub(),
webView: webView
)
messageHandler.delegate = delegate

// This should not crash and should not trigger any delegate events
messageHandler.handle(.hapticFeedback(hapticType: "medium"))

// Haptic feedback doesn't trigger delegate events, so we just verify it doesn't crash
#expect(delegate.eventDidOccur == nil)
}
}
Loading