Skip to content

Commit

Permalink
Merge pull request #523 from pennlabs/anli/banners-2024
Browse files Browse the repository at this point in the history
Leveraging User Engagement Through Dynamic Banner Integration
  • Loading branch information
anli5005 authored Mar 29, 2024
2 parents b0d1f13 + 1467d84 commit d929b30
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 2 deletions.
24 changes: 24 additions & 0 deletions PennMobile.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
8766844128CBE907005CAD32 /* NativeNewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8766844028CBE907005CAD32 /* NativeNewsViewController.swift */; };
87FE6479290EE4BE00AFADF6 /* NotificationAPIModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87FE6478290EE4BE00AFADF6 /* NotificationAPIModel.swift */; };
890471732B76E800001FF257 /* Day.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890471722B76E800001FF257 /* Day.swift */; };
89093EE22BB4FEB90076B712 /* UserEngagementPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89093EE12BB4FEB90076B712 /* UserEngagementPopupView.swift */; };
890C4EC82ACBA486009650CA /* LaundryAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C4EC62ACBA486009650CA /* LaundryAttributes.swift */; };
890C4EC92ACBA486009650CA /* LaundryMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C4EC72ACBA486009650CA /* LaundryMachine.swift */; };
890C4ECA2ACBA4E7009650CA /* LaundryLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8932693B28FC75A5003D4BF9 /* LaundryLiveActivity.swift */; };
Expand Down Expand Up @@ -256,6 +257,7 @@
895C75E628FA165100A329A0 /* LabsLoginSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895C75E528FA165100A329A0 /* LabsLoginSwiftUI.swift */; };
895D98B02ACF312300F8C5DF /* MoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895D98AF2ACF312300F8C5DF /* MoreView.swift */; };
897F0A932B08256C0060583A /* Toasts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897F0A922B08256C0060583A /* Toasts.swift */; };
8987C8FD29CE5A8B004E9A99 /* BannerDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8987C8FC29CE5A8B004E9A99 /* BannerDescription.swift */; };
898DB4912B2E7AA20027CC8F /* PennForms in Frameworks */ = {isa = PBXBuildFile; productRef = 898DB4902B2E7AA20027CC8F /* PennForms */; };
89913EAD2AE44FCE00AE30C9 /* CalendarCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89913EAC2AE44FCE00AE30C9 /* CalendarCardView.swift */; };
89B454DF28E1161B00BC918B /* PathAtPennNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89B454DE28E1161B00BC918B /* PathAtPennNetworkManager.swift */; };
Expand All @@ -265,6 +267,8 @@
89CA729129174CF900CF72FE /* DiningAnalyticsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CA727229171E3900CF72FE /* DiningAnalyticsProvider.swift */; };
89CA72952917541C00CF72FE /* DiningAnalyticsHomeWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CA7292291753DF00CF72FE /* DiningAnalyticsHomeWidget.swift */; };
89DCBEC628E791B70029F784 /* CoursesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89DCBEC528E791B70029F784 /* CoursesViewController.swift */; };
89DF550A29CE49A900EF03F7 /* BannerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89DF550929CE49A900EF03F7 /* BannerViewModel.swift */; };
89DF550C29CE4BDF00EF03F7 /* BannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89DF550B29CE4BDF00EF03F7 /* BannerView.swift */; };
89DF9C352B66C3A700267449 /* Multipart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89DF9C342B66C3A700267449 /* Multipart.swift */; };
89E0DE682AE38A8800E918FF /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89E0DE672AE38A8800E918FF /* HomeView.swift */; };
89E0DE6A2AE396E200E918FF /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89E0DE692AE396E200E918FF /* HomeViewModel.swift */; };
Expand Down Expand Up @@ -657,6 +661,7 @@
8766844028CBE907005CAD32 /* NativeNewsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeNewsViewController.swift; sourceTree = "<group>"; };
87FE6478290EE4BE00AFADF6 /* NotificationAPIModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAPIModel.swift; sourceTree = "<group>"; };
890471722B76E800001FF257 /* Day.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Day.swift; sourceTree = "<group>"; };
89093EE12BB4FEB90076B712 /* UserEngagementPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEngagementPopupView.swift; sourceTree = "<group>"; };
890C4EC62ACBA486009650CA /* LaundryAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaundryAttributes.swift; sourceTree = "<group>"; };
890C4EC72ACBA486009650CA /* LaundryMachine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaundryMachine.swift; sourceTree = "<group>"; };
890D14FB2AB7469300672FFE /* PennMobile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PennMobile.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -686,6 +691,7 @@
895C75E528FA165100A329A0 /* LabsLoginSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsLoginSwiftUI.swift; sourceTree = "<group>"; };
895D98AF2ACF312300F8C5DF /* MoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreView.swift; sourceTree = "<group>"; };
897F0A922B08256C0060583A /* Toasts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toasts.swift; sourceTree = "<group>"; };
8987C8FC29CE5A8B004E9A99 /* BannerDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerDescription.swift; sourceTree = "<group>"; };
89913EAC2AE44FCE00AE30C9 /* CalendarCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarCardView.swift; sourceTree = "<group>"; };
89B454DE28E1161B00BC918B /* PathAtPennNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathAtPennNetworkManager.swift; sourceTree = "<group>"; wrapsLines = 0; };
89CA5FBE2AD315E400B7D3EF /* ProfileRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRowView.swift; sourceTree = "<group>"; };
Expand All @@ -695,6 +701,8 @@
89CA7292291753DF00CF72FE /* DiningAnalyticsHomeWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningAnalyticsHomeWidget.swift; sourceTree = "<group>"; };
89CA72962917584000CF72FE /* MeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterView.swift; sourceTree = "<group>"; };
89DCBEC528E791B70029F784 /* CoursesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursesViewController.swift; sourceTree = "<group>"; };
89DF550929CE49A900EF03F7 /* BannerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerViewModel.swift; sourceTree = "<group>"; };
89DF550B29CE4BDF00EF03F7 /* BannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerView.swift; sourceTree = "<group>"; };
89DF9C342B66C3A700267449 /* Multipart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Multipart.swift; sourceTree = "<group>"; };
89E0DE672AE38A8800E918FF /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
89E0DE692AE396E200E918FF /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1001,6 +1009,7 @@
children = (
E49D3B8C2B66C6D20022AB71 /* Subletting */,
211B11A41F2995AD00A22C8A /* PennMobile.entitlements */,
89DF550829CE499B00EF03F7 /* Banners */,
CF29A15F1FB7873A0067D946 /* Onboarding */,
216640C71EBADC9D00746B8E /* General */,
21B653BC2246F31E001A97C5 /* Courses */,
Expand Down Expand Up @@ -1724,6 +1733,17 @@
path = Views;
sourceTree = "<group>";
};
89DF550829CE499B00EF03F7 /* Banners */ = {
isa = PBXGroup;
children = (
89DF550929CE49A900EF03F7 /* BannerViewModel.swift */,
89DF550B29CE4BDF00EF03F7 /* BannerView.swift */,
89093EE12BB4FEB90076B712 /* UserEngagementPopupView.swift */,
8987C8FC29CE5A8B004E9A99 /* BannerDescription.swift */,
);
path = Banners;
sourceTree = "<group>";
};
89EA261F290EE39E008F26CF /* Courses */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2525,6 +2545,7 @@
6C6035FC26E723240025FBC7 /* EmptyView.swift in Sources */,
21640FD3204A296D008DB6E8 /* LaundryGraphView.swift in Sources */,
97E6E1F0239D74F500C07D7A /* GSRGroupIconView.swift in Sources */,
89093EE22BB4FEB90076B712 /* UserEngagementPopupView.swift in Sources */,
F212BE8423B6C8A200ED46A1 /* NotificationPreference.swift in Sources */,
429EA1E12B8BA18300824455 /* SublettingAPI.swift in Sources */,
21A6B6D022162652003A357D /* GSRReservationsController.swift in Sources */,
Expand Down Expand Up @@ -2656,6 +2677,7 @@
B6040A361F8F24D900E4B783 /* AddLaundryCell.swift in Sources */,
219F01F61FB7E14B006BBC4E /* SelectionCell.swift in Sources */,
89913EAD2AE44FCE00AE30C9 /* CalendarCardView.swift in Sources */,
89DF550A29CE49A900EF03F7 /* BannerViewModel.swift in Sources */,
2190FD351EC625BB00EC683C /* Protocols.swift in Sources */,
2189C0A82027CE4B00771C1F /* ThumbLayer.swift in Sources */,
E735C9422AF81498000F7376 /* DiningSettingsView.swift in Sources */,
Expand All @@ -2669,6 +2691,7 @@
2189C0872027CDD700771C1F /* GSRNetworkManager.swift in Sources */,
2138D55522598AA800D67CA2 /* GSRTabController.swift in Sources */,
6CFA06F826E8355400944B8E /* HomeFeatureCellItem.swift in Sources */,
89DF550C29CE4BDF00EF03F7 /* BannerView.swift in Sources */,
21640D5E20105BAC002F33CA /* HomeLaundryCell.swift in Sources */,
6CAA4B5727A763A400473CC6 /* HomePollsCell.swift in Sources */,
F27AA01A23BC6D1400276C4F /* PrivacyPermissionDelegate.swift in Sources */,
Expand All @@ -2677,6 +2700,7 @@
2130A5062238C2F000DFEEC7 /* PennLoginController.swift in Sources */,
21508166220D2499002F7EA1 /* HomeNewsCellItem.swift in Sources */,
2189C0912027CE2E00771C1F /* GSRViewModel.swift in Sources */,
8987C8FD29CE5A8B004E9A99 /* BannerDescription.swift in Sources */,
C15C4B4E223EB16F00E443FD /* HomeReservationsCell.swift in Sources */,
B62875F92118F95300FB2873 /* BuildingProtocol.swift in Sources */,
F212BE8623B6DA8D00ED46A1 /* PrivacyTableViewCell.swift in Sources */,
Expand Down
13 changes: 13 additions & 0 deletions PennMobile/Banners/BannerDescription.swift
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?
}
72 changes: 72 additions & 0 deletions PennMobile/Banners/BannerView.swift
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()
}
}
}
82 changes: 82 additions & 0 deletions PennMobile/Banners/BannerViewModel.swift
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
}
}
}
46 changes: 46 additions & 0 deletions PennMobile/Banners/UserEngagementPopupView.swift
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()
}
}
16 changes: 16 additions & 0 deletions PennMobile/Home/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct HomeView<Model: HomeViewModel>: View {

@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var viewModel: Model
@EnvironmentObject var bannerViewModel: BannerViewModel

var dateFormatStyle: Date.FormatStyle {
Date.FormatStyle()
Expand Down Expand Up @@ -51,9 +52,23 @@ struct HomeView<Model: HomeViewModel>: View {
.padding(.bottom)
.multilineTextAlignment(.center)

if bannerViewModel.showBanners {
BannerView()
.frame(maxWidth: .infinity)
.frame(width: 0)
.padding(.bottom)
}

viewModel.data.content(for: context.date)
.frame(maxWidth: 480)
.frame(maxWidth: .infinity)

if bannerViewModel.showBanners {
BannerView()
.frame(maxWidth: .infinity)
.frame(width: 0)
.padding(.top)
}
}
.padding(.bottom)
// Hack for forcing the navbar to always render
Expand Down Expand Up @@ -97,4 +112,5 @@ struct HomeView<Model: HomeViewModel>: View {
#Preview {
HomeView<MockHomeViewModel>()
.environmentObject(MockHomeViewModel())
.environmentObject(BannerViewModel.shared)
}
11 changes: 11 additions & 0 deletions PennMobile/More Tab/MoreView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ struct MoreView: View {

@EnvironmentObject var authManager: AuthManager
@EnvironmentObject var navigationManager: NavigationManager
@EnvironmentObject var bannerViewModel: BannerViewModel

@State var isPresentingLoginSheet = false
@State var isLoggingOut = false
Expand Down Expand Up @@ -121,6 +122,16 @@ struct MoreView: View {
} header: {
Text("Links")
}

if Account.getAccount()?.pennid == 12345678 {
Section {
Toggle(isOn: $bannerViewModel.showBanners) {
Text("Force April Fools")
}
} header: {
Text("Debugging")
}
}
}
.navigationTitle(Text("More"))
.navigationBarTitleDisplayMode(.inline)
Expand Down
1 change: 1 addition & 0 deletions PennMobile/Setup + Navigation/PennMobile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ struct PennMobile: App {
RootView()
.environmentObject(authManager)
.environmentObject(homeViewModel)
.environmentObject(BannerViewModel.shared)
#if DEBUG
.environmentObject(mockHomeViewModel)
#endif
Expand Down
Loading

0 comments on commit d929b30

Please sign in to comment.