Skip to content

Commit

Permalink
[Paywalls V2] Add purchase button activity indicator (#4787)
Browse files Browse the repository at this point in the history
* This mostly works

* Improved the code and added preview

* Add some comments and cleanup code

* More lint

* Make word for macos

* Maybe this

* This will work
  • Loading branch information
joshdholtz authored Feb 14, 2025
1 parent 7683d52 commit 6331687
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 2 deletions.
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
030F918A2D55C1D20085103F /* LocaleFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F91892D55C1AB0085103F /* LocaleFinder.swift */; };
030F918C2D55C9DC0085103F /* LocaleFinderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F918B2D55C9D80085103F /* LocaleFinderTests.swift */; };
030F918E2D5664410085103F /* LocaleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F918D2D5664410085103F /* LocaleExtensions.swift */; };
030F93B32D5ED90B0085103F /* ProgressViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F93B22D5ED90B0085103F /* ProgressViewModifier.swift */; };
0313FD41268A506400168386 /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313FD40268A506400168386 /* DateProvider.swift */; };
0354AA462D4029C300F9E330 /* TabControlButtonComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0354AA3E2D4029C300F9E330 /* TabControlButtonComponentViewModel.swift */; };
0354AA472D4029C300F9E330 /* TabControlComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0354AA402D4029C300F9E330 /* TabControlComponentViewModel.swift */; };
Expand Down Expand Up @@ -1295,6 +1296,7 @@
030F91892D55C1AB0085103F /* LocaleFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleFinder.swift; sourceTree = "<group>"; };
030F918B2D55C9D80085103F /* LocaleFinderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleFinderTests.swift; sourceTree = "<group>"; };
030F918D2D5664410085103F /* LocaleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleExtensions.swift; sourceTree = "<group>"; };
030F93B22D5ED90B0085103F /* ProgressViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressViewModifier.swift; sourceTree = "<group>"; };
0313FD40268A506400168386 /* DateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProvider.swift; sourceTree = "<group>"; };
0354AA3D2D4029C300F9E330 /* TabControlButtonComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabControlButtonComponentView.swift; sourceTree = "<group>"; };
0354AA3E2D4029C300F9E330 /* TabControlButtonComponentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabControlButtonComponentViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2687,6 +2689,7 @@
2C7457442CEA652B004ACE52 /* ViewHelpers */ = {
isa = PBXGroup;
children = (
030F93B22D5ED90B0085103F /* ProgressViewModifier.swift */,
2C7457872CEDF7AC004ACE52 /* BackgroundStyle.swift */,
77089F9D2CD39EC100848CD5 /* ShadowModifier.swift */,
4D6F4BCF2CF69DE300353AF6 /* ForegroundColorScheme.swift */,
Expand Down Expand Up @@ -6900,6 +6903,7 @@
357CEC702C5940CE00A80837 /* ColorFromAppearance.swift in Sources */,
887A60832C1D037000E1A461 /* VersionDetector.swift in Sources */,
574D1C702D3E75F9005840CD /* PurchaseDetailView.swift in Sources */,
030F93B32D5ED90B0085103F /* ProgressViewModifier.swift in Sources */,
574D1C712D3E75F9005840CD /* PurchaseHistoryView.swift in Sources */,
574D1C722D3E75F9005840CD /* PurchaseLinkView.swift in Sources */,
88B1BAFC2C813A3C001B7EE5 /* ImageComponentView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@ struct PurchaseButtonComponentView: View {
_ = try await self.purchaseHandler.purchase(package: selectedPackage)
} label: {
// Not passing an onDismiss - nothing in this stack should be able to dismiss
StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: {})
StackComponentView(
viewModel: viewModel.stackViewModel,
onDismiss: {},
showActivityIndicatorOverContent: self.purchaseHandler.actionInProgress
)
}
.disabled(self.purchaseHandler.actionInProgress)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,29 @@ struct StackComponentView: View {
@Environment(\.screenCondition)
private var screenCondition

@Environment(\.colorScheme)
private var colorScheme

private let viewModel: StackComponentViewModel
private let isScrollableByDefault: Bool
private let onDismiss: () -> Void
/// Used when this stack needs more padding than defined in the component, e.g. to avoid being drawn in the safe
/// area when displayed as a sticky footer.
private let additionalPadding: EdgeInsets
private let showActivityIndicatorOverContent: Bool

init(
viewModel: StackComponentViewModel,
isScrollableByDefault: Bool = false,
onDismiss: @escaping () -> Void,
additionalPadding: EdgeInsets? = nil
additionalPadding: EdgeInsets? = nil,
showActivityIndicatorOverContent: Bool = false
) {
self.viewModel = viewModel
self.isScrollableByDefault = isScrollableByDefault
self.onDismiss = onDismiss
self.additionalPadding = additionalPadding ?? EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
self.showActivityIndicatorOverContent = showActivityIndicatorOverContent
}

var body: some View {
Expand All @@ -67,6 +73,7 @@ struct StackComponentView: View {
}

@ViewBuilder
// swiftlint:disable:next function_body_length
private func make(style: StackComponentStyle) -> some View {
Group {
switch style.dimension {
Expand Down Expand Up @@ -109,8 +116,12 @@ struct StackComponentView: View {
verticalAlignment: alignment.stackAlignment)
}
}
.hidden(if: self.showActivityIndicatorOverContent)
.padding(style.padding)
.padding(additionalPadding)
.applyIf(self.showActivityIndicatorOverContent, apply: { view in
view.progressOverlay(for: style.backgroundStyle)
})
.shape(border: nil,
shape: style.shape,
background: style.backgroundStyle,
Expand Down Expand Up @@ -495,6 +506,50 @@ struct StackComponentView_Previews: PreviewProvider {
.previewLayout(.sizeThatFits)
.previewDisplayName("Scrollable - HStack")

// Progress
let colorOptions: [(String, String, PaywallComponent.ColorInfo)] = [
("Solid color - white tint", "#ffffff", .hex("#ff0000")),
("Solid color - black tint", "#000000", .hex("#f784ff")),
("Gradient - white tint", "#ffffff", .linear(0, [
.init(color: "#1a2494", percent: 0),
.init(color: "#380303", percent: 80)
])),
("Gradient - black tint", "#000000", .linear(0, [
.init(color: "#d6ea92", percent: 0),
.init(color: "#6cacef", percent: 80)
]))
]
ForEach(colorOptions, id: \.self.0) { colorPair in
StackComponentView(
// swiftlint:disable:next force_try
viewModel: try! .init(
component: .init(
components: [
.text(.init(
text: "text_1",
color: .init(light: .hex(colorPair.1))))
],
size: .init(
width: .fill,
height: .fixed(100)
),
backgroundColor: .init(light: colorPair.2)
),
localizationProvider: .init(
locale: Locale.current,
localizedStrings: [
"text_1": .string("Hey")
]
)
),
onDismiss: {},
showActivityIndicatorOverContent: true
)
.previewRequiredEnvironmentProperties()
.previewLayout(.sizeThatFits)
.previewDisplayName("Progress - \(colorPair.0)")
}

// Fits don't expand
StackComponentView(
// swiftlint:disable:next force_try
Expand Down
100 changes: 100 additions & 0 deletions RevenueCatUI/Templates/V2/ViewHelpers/ProgressViewModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// ProgressViewModifier.swift
//
// Created by Josh Holtz on 2/13/25.

#if !os(macOS) && !os(tvOS) // For Paywalls V2

import SwiftUI

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct ProgressViewModifier: ViewModifier {

@Environment(\.colorScheme)
private var colorScheme

var backgroundStyle: BackgroundStyle?

func body(content: Content) -> some View {
content
#if !os(watchOS)
.background(.ultraThinMaterial)
#endif
.overlay(progressView)
}

@ViewBuilder
private var progressView: some View {
switch backgroundStyle {
case .color(let displayableColorScheme):
let colorInfo = displayableColorScheme.effectiveColor(for: colorScheme)
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: bestTint(for: colorInfo)))
case .image, .none:
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
}

private func bestTint(for colorInfo: DisplayableColorInfo) -> Color {
switch colorInfo {
case .hex:
return colorInfo.toColor(fallback: .black).brightness() > 0.6 ? .black : .white
case .linear, .radial:
let gradient = colorInfo.toGradient()
let averageBrightness = gradient.stops
.compactMap { $0.color.brightness() }
.reduce(0, +) / CGFloat(gradient.stops.count)
return averageBrightness > 0.6 ? .black : .white
}
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
private extension Color {

/// Calculates the perceived brightness of the color.
/// Uses the standard luminance formula for relative brightness perception.
func brightness() -> CGFloat {
#if os(macOS)
guard let nsColor = NSColor(self).usingColorSpace(.deviceRGB) else { return 1.0 }
let red = nsColor.redComponent
let green = nsColor.greenComponent
let blue = nsColor.blueComponent
#else
guard let uiColor = UIColor(self).cgColor.components, uiColor.count >= 3 else { return 1.0 }
let red = uiColor[0]
let green = uiColor[1]
let blue = uiColor[2]
#endif

// Standard luminance coefficients for sRGB (per ITU-R BT.709)
let redCoefficient: CGFloat = 0.299
let greenCoefficient: CGFloat = 0.587
let blueCoefficient: CGFloat = 0.114

// Compute brightness using the weighted sum of RGB components
return (red * redCoefficient) + (green * greenCoefficient) + (blue * blueCoefficient)
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension View {

func progressOverlay(for backgroundStyle: BackgroundStyle?) -> some View {
self.modifier(ProgressViewModifier(backgroundStyle: backgroundStyle))
}

}

#endif

0 comments on commit 6331687

Please sign in to comment.