-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #523 from pennlabs/anli/banners-2024
Leveraging User Engagement Through Dynamic Banner Integration
- Loading branch information
Showing
9 changed files
with
292 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// | ||
// BannerDescription.swift | ||
// PennMobile | ||
// | ||
// Created by Anthony Li on 3/24/23. | ||
// Copyright © 2023 PennLabs. All rights reserved. | ||
// | ||
|
||
struct BannerDescription: Equatable, Codable { | ||
var image: URL | ||
var text: String | ||
var action: URL? | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// | ||
// BannerView.swift | ||
// PennMobile | ||
// | ||
// Created by Anthony Li on 3/24/23. | ||
// Copyright © 2023 PennLabs. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
import Kingfisher | ||
|
||
extension BannerDescription: View { | ||
var imageView: some View { | ||
KFImage(image) | ||
.resizable() | ||
.scaledToFill() | ||
.accessibilityLabel(Text(text)) | ||
} | ||
|
||
var body: some View { | ||
Group { | ||
if let action { | ||
Link(destination: action) { | ||
imageView | ||
} | ||
} else { | ||
imageView | ||
} | ||
} | ||
.id(image) | ||
.transition(.slide) | ||
} | ||
} | ||
|
||
struct BannerView: View { | ||
static let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() | ||
static let height: CGFloat = 96 | ||
|
||
@EnvironmentObject var viewModel: BannerViewModel | ||
@State var banner: BannerDescription? | ||
|
||
func selectBanner() { | ||
banner = viewModel.banners.random | ||
} | ||
|
||
var body: some View { | ||
Group { | ||
if let banner { | ||
banner | ||
} else { | ||
Text("Finding personalized offers for you...") | ||
.font(.custom("Arial", size: 16)) | ||
.foregroundColor(.uiBackground) | ||
.padding() | ||
} | ||
} | ||
.frame(maxWidth: .infinity, maxHeight: .infinity) | ||
.background(Color.primary) | ||
.frame(height: BannerView.height) | ||
.clipped() | ||
.ignoresSafeArea() | ||
.onReceive(BannerView.timer) { _ in | ||
withAnimation { | ||
selectBanner() | ||
} | ||
} | ||
.onAppear { | ||
viewModel.fetchBannersIfNeeded() | ||
selectBanner() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
// | ||
// BannerViewModel.swift | ||
// PennMobile | ||
// | ||
// Created by Anthony Li on 3/24/23. | ||
// Copyright © 2023 PennLabs. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
import Foundation | ||
|
||
private func getDefaultBannerURL() -> URL { | ||
let data = Data(base64Encoded: "aHR0cHM6Ly9wZW5ubGFicy5naXRodWIuaW8vcGxhdGZvcm0tc2FtcGxlLWFzc2V0cy9hc3NldHMuanNvbg==")! | ||
return URL(string: String(data: data, encoding: .ascii)!)! | ||
} | ||
|
||
func getDefaultPopupURL() -> URL { | ||
let data = Data(base64Encoded: "aHR0cHM6Ly9wZW5ubGFicy5naXRodWIuaW8vcGxhdGZvcm0tc2FtcGxlLWFzc2V0cy9pbnRlcmFjdGl2ZS5odG1sCg==")! | ||
return URL(string: String(data: data, encoding: .ascii)!)! | ||
} | ||
|
||
@MainActor class BannerViewModel: ObservableObject { | ||
static let shared = BannerViewModel( | ||
url: getDefaultBannerURL(), | ||
cacheMaxAge: 60 * 60 | ||
) | ||
|
||
static var isAprilFools: Bool { | ||
let components = Calendar.autoupdatingCurrent.dateComponents(in: .autoupdatingCurrent, from: Date()) | ||
return components.month == 4 && components.day == 1 | ||
} | ||
|
||
@Published var banners: [BannerDescription] = [] | ||
private var isFetching = false | ||
private var lastSuccessfulFetch: Date? | ||
|
||
@Published var showBanners = ProcessInfo.processInfo.environment["FORCE_BANNERS"] != nil || BannerViewModel.isAprilFools | ||
@Published var showPopup = true | ||
|
||
let url: URL | ||
let cacheMaxAge: TimeInterval | ||
|
||
init(url: URL, cacheMaxAge: TimeInterval) { | ||
self.url = url | ||
self.cacheMaxAge = cacheMaxAge | ||
} | ||
|
||
let decoder = { | ||
let decoder = JSONDecoder() | ||
decoder.allowsJSON5 = true | ||
return decoder | ||
}() | ||
|
||
func fetchBannersIfNeeded() { | ||
if isFetching { | ||
return | ||
} | ||
|
||
if let lastSuccessfulFetch, -lastSuccessfulFetch.timeIntervalSinceNow < cacheMaxAge { | ||
return | ||
} | ||
|
||
struct BannerResponse: Decodable { | ||
let assets: [BannerDescription] | ||
} | ||
|
||
isFetching = true | ||
Task { | ||
do { | ||
let (data, _) = try await URLSession(configuration: .ephemeral).data(from: url) | ||
let response = try decoder.decode(BannerResponse.self, from: data) | ||
banners = response.assets | ||
lastSuccessfulFetch = Date() | ||
} catch let error { | ||
lastSuccessfulFetch = nil | ||
print("Failed to load banners: \(error)") | ||
} | ||
|
||
isFetching = false | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// | ||
// UserEngagementPopupView.swift | ||
// PennMobile | ||
// | ||
// Created by Anthony Li on 3/27/24. | ||
// Copyright © 2024 PennLabs. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
struct UserEngagementPopupView: View { | ||
static func randomAlignment() -> Alignment { | ||
[.topTrailing, .bottomTrailing, .bottomLeading, .bottom, .top, .topLeading].randomElement()! | ||
} | ||
|
||
@State var alignment: Alignment = Self.randomAlignment() | ||
@EnvironmentObject var bannerViewModel: BannerViewModel | ||
@State var remainingDismissAttempts = 1 | ||
|
||
var body: some View { | ||
ZStack(alignment: alignment) { | ||
VStack(spacing: 0) { | ||
Text("A personalized offer just for you!") | ||
.font(.caption) | ||
WebView(url: getDefaultPopupURL()) | ||
} | ||
.ignoresSafeArea() | ||
|
||
Button { | ||
if remainingDismissAttempts <= 0 { | ||
bannerViewModel.showPopup = false | ||
} else { | ||
remainingDismissAttempts -= 1 | ||
self.alignment = Self.randomAlignment() | ||
} | ||
} label: { | ||
Image(systemName: "xmark.circle.fill") | ||
.symbolRenderingMode(.palette) | ||
.foregroundStyle(.white, .black) | ||
} | ||
.accessibilityLabel("Close") | ||
.padding() | ||
} | ||
.interactiveDismissDisabled() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.