Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exploration: Use Gravatar SDK's profile view in account settings #1824

Draft
wants to merge 9 commits into
base: trunk
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public enum FeatureFlag: String, CaseIterable {
/// show UpNext tab on the main tab bar
case upNextOnTabBar

/// Enhances the profile view to display more fields from the user's Gravatar profile.
case displayGravatarProfile

/// When enabled it updates the code on filter callback to use a safer method to convert unmanaged player references
/// This is to fix this: https://a8c.sentry.io/share/issue/39a6d2958b674ec3b7a4d9248b4b5ffa/
case defaultPlayerFilterCallbackFix
Expand Down Expand Up @@ -112,6 +115,8 @@ public enum FeatureFlag: String, CaseIterable {
true
case .upNextOnTabBar:
true
case .displayGravatarProfile:
true
case .downloadFixes:
true
case .onlyMarkPodcastsUnsyncedForNewUsers:
Expand Down
39 changes: 39 additions & 0 deletions podcasts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,10 @@
8BF1C61D2881F05D007E80BF /* FolderModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF1C61C2881F05D007E80BF /* FolderModelTests.swift */; };
8BF9CC0C2ADDA90F004E9B65 /* YearOverYearStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF9CC0B2ADDA90F004E9B65 /* YearOverYearStory.swift */; };
8BFB434E2A1FFB4B00F3D409 /* StatusPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFB434D2A1FFB4B00F3D409 /* StatusPageViewModel.swift */; };
91319B512C1984AD000220A4 /* Gravatar in Frameworks */ = {isa = PBXBuildFile; productRef = 91319B502C1984AD000220A4 /* Gravatar */; };
91319B532C1984AD000220A4 /* GravatarUI in Frameworks */ = {isa = PBXBuildFile; productRef = 91319B522C1984AD000220A4 /* GravatarUI */; };
91319B562C19E838000220A4 /* Gravatar in Frameworks */ = {isa = PBXBuildFile; productRef = 91319B552C19E838000220A4 /* Gravatar */; };
91319B582C19E838000220A4 /* GravatarUI in Frameworks */ = {isa = PBXBuildFile; productRef = 91319B572C19E838000220A4 /* GravatarUI */; };
BD001B892174260B00504DD3 /* FilterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD001B882174260B00504DD3 /* FilterManager.swift */; };
BD00CB2B24BD20CD00A10257 /* TimeStepperCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD00CB2924BD20CD00A10257 /* TimeStepperCell.swift */; };
BD00CB2C24BD20CD00A10257 /* TimeStepperCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BD00CB2A24BD20CD00A10257 /* TimeStepperCell.xib */; };
Expand Down Expand Up @@ -3552,12 +3556,15 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
91319B512C1984AD000220A4 /* Gravatar in Frameworks */,
46C22EE028F73D9A00F4173B /* AuthenticationServices.framework in Frameworks */,
468F00D327CD575C00FFAAAA /* FirebasePerformance in Frameworks */,
BD11C41D2524741C00E308E6 /* CarPlay.framework in Frameworks */,
BD44F750267C850000810148 /* DifferenceKit in Frameworks */,
C72CED34289DA28B0017883A /* AutomatticTracks in Frameworks */,
91319B532C1984AD000220A4 /* GravatarUI in Frameworks */,
BDD8AABF267C858D0013494D /* Fuse in Frameworks */,
91319B562C19E838000220A4 /* Gravatar in Frameworks */,
46305CF0272B082C003AC87B /* AutomatticEncryptedLogs in Frameworks */,
468F00CF27CD575C00FFAAAA /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */,
8BAD6E5E2975ADB800DB7259 /* GoogleSignIn in Frameworks */,
Expand All @@ -3568,6 +3575,7 @@
8B365E502B62E82000143DAC /* Agrume in Frameworks */,
8BE36E002873552500E35313 /* PocketCastsServer in Frameworks */,
BDD239BD267C869B0047750C /* SwipeCellKit in Frameworks */,
91319B582C19E838000220A4 /* GravatarUI in Frameworks */,
8B1762772B6808F700F44450 /* JLRoutes in Frameworks */,
8B17627A2B684E7100F44450 /* Kingfisher in Frameworks */,
1C312783007F5948E428F709 /* libPods-podcasts.a in Frameworks */,
Expand Down Expand Up @@ -7630,6 +7638,10 @@
8B365E4F2B62E82000143DAC /* Agrume */,
8B1762762B6808F700F44450 /* JLRoutes */,
8B1762792B684E7100F44450 /* Kingfisher */,
91319B502C1984AD000220A4 /* Gravatar */,
91319B522C1984AD000220A4 /* GravatarUI */,
91319B552C19E838000220A4 /* Gravatar */,
91319B572C19E838000220A4 /* GravatarUI */,
);
productName = podcasts;
productReference = BDBD53EC17019B2A0048C8C5 /* podcasts.app */;
Expand Down Expand Up @@ -7796,6 +7808,7 @@
8B365E4E2B62E82000143DAC /* XCRemoteSwiftPackageReference "Agrume" */,
8B1762752B6808F700F44450 /* XCRemoteSwiftPackageReference "JLRoutes" */,
8B1762782B684E7100F44450 /* XCRemoteSwiftPackageReference "Kingfisher" */,
91319B542C19E837000220A4 /* XCRemoteSwiftPackageReference "Gravatar-SDK-iOS" */,
);
productRefGroup = BDBD53ED17019B2A0048C8C5 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -11848,6 +11861,14 @@
minimumVersion = 5.6.12;
};
};
91319B542C19E837000220A4 /* XCRemoteSwiftPackageReference "Gravatar-SDK-iOS" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Automattic/Gravatar-SDK-iOS.git";
requirement = {
kind = revision;
revision = afe715820691a17f286fac9891afc5290d0e2981;
};
};
BD44F74E267C850000810148 /* XCRemoteSwiftPackageReference "DifferenceKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ra1028/DifferenceKit";
Expand Down Expand Up @@ -11977,6 +11998,24 @@
isa = XCSwiftPackageProductDependency;
productName = PocketCastsDataModel;
};
91319B502C1984AD000220A4 /* Gravatar */ = {
isa = XCSwiftPackageProductDependency;
productName = Gravatar;
};
91319B522C1984AD000220A4 /* GravatarUI */ = {
isa = XCSwiftPackageProductDependency;
productName = GravatarUI;
};
91319B552C19E838000220A4 /* Gravatar */ = {
isa = XCSwiftPackageProductDependency;
package = 91319B542C19E837000220A4 /* XCRemoteSwiftPackageReference "Gravatar-SDK-iOS" */;
productName = Gravatar;
};
91319B572C19E838000220A4 /* GravatarUI */ = {
isa = XCSwiftPackageProductDependency;
package = 91319B542C19E837000220A4 /* XCRemoteSwiftPackageReference "Gravatar-SDK-iOS" */;
productName = GravatarUI;
};
BD44F74F267C850000810148 /* DifferenceKit */ = {
isa = XCSwiftPackageProductDependency;
package = BD44F74E267C850000810148 /* XCRemoteSwiftPackageReference "DifferenceKit" */;
Expand Down
9 changes: 9 additions & 0 deletions podcasts.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@
"version": "7.13.1"
}
},
{
"package": "Gravatar",
"repositoryURL": "https://github.com/Automattic/Gravatar-SDK-iOS.git",
"state": {
"branch": null,
"revision": "afe715820691a17f286fac9891afc5290d0e2981",
"version": null
}
},
{
"package": "gRPC",
"repositoryURL": "https://github.com/google/grpc-binary.git",
Expand Down
194 changes: 190 additions & 4 deletions podcasts/AccountViewController.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import Combine
import GravatarUI
import PocketCastsServer
import PocketCastsUtils
import UIKit
import SafariServices

class AccountViewController: UIViewController, ChangeEmailDelegate {
enum UIConstants {
enum Gravatar {
static let padding: UIEdgeInsets = .init(top: 12, left: 12, bottom: 12, right: 12)
}
}
enum TableRow { case upgradeView, changeEmail, changePassword, upgradeAccount, newsletter, cancelSubscription, logout, deleteAccount, privacyPolicy, termsOfUse, supporterContributions }
var tableData: [[TableRow]] = [[.changeEmail, .changePassword, .newsletter], [.privacyPolicy, .termsOfUse], [.logout], [.deleteAccount]]

Expand All @@ -12,7 +20,6 @@ class AccountViewController: UIViewController, ChangeEmailDelegate {
private var isUsernamePasswordLogin: Bool {
ServerSettings.syncingPassword() != nil
}

@IBOutlet var tableView: ThemeableTable! {
didSet {
tableView.applyInsetForMiniPlayer()
Expand All @@ -38,6 +45,52 @@ class AccountViewController: UIViewController, ChangeEmailDelegate {
return viewModel
}()

lazy var headerStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [gravatarProfileViewContainer, updatedHeaderContentView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .fill
stackView.axis = .vertical
return stackView
}()

private lazy var gravatarProfileViewContainer: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(gravatarProfileView)
let horizontalPadding: CGFloat = 16
let verticalPadding: CGFloat = 16
NSLayoutConstraint.activate([
gravatarProfileView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -horizontalPadding),
gravatarProfileView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: horizontalPadding),
gravatarProfileView.topAnchor.constraint(equalTo: view.topAnchor, constant: verticalPadding),
gravatarProfileView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -verticalPadding)
])
return view
}()

private lazy var gravatarProfileView: UIView & UIContentView = {
let contentView = LargeProfileSummaryView(avatarType: .custom(self))
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.layer.cornerRadius = 8
contentView.clipsToBounds = true
return contentView
}()

private let gravatarViewModel: ProfileViewModel = .init()

private var cancellables = Set<AnyCancellable>()

private lazy var gravatarConfiguration: ProfileViewConfiguration = {
return ProfileViewConfiguration.largeSummary().updatedForPocketCasts(delegate: self)
}() {
didSet {
gravatarProfileView.configuration = gravatarConfiguration
tableView.setNeedsLayout()
tableView.layoutIfNeeded()
tableView.reloadData()
}
}

lazy var updatedHeaderContentView: UIView = {
let headerView = AccountHeaderView(viewModel: headerViewModel)

Expand All @@ -47,6 +100,21 @@ class AccountViewController: UIViewController, ChangeEmailDelegate {
return view
}()

private lazy var subscriptionAvatarView: UIView = {
let view = SubscriptionProfileImage(viewModel: headerViewModel)
.frame(width: ProfileHeaderView.Constants.imageSize, height: ProfileHeaderView.Constants.imageSize)
let avatarView = view.themedUIView
avatarView.backgroundColor = .clear
return avatarView
}()

private func toggleGravatarProfileView() {
gravatarProfileViewContainer.isHidden = !headerViewModel.shouldDisplayGravatarProfile
if headerViewModel.shouldDisplayGravatarProfile {
fetchGravatarProfile()
}
}

override func viewDidLoad() {
super.viewDidLoad()
title = L10n.accountTitle
Expand All @@ -55,8 +123,9 @@ class AccountViewController: UIViewController, ChangeEmailDelegate {
NotificationCenter.default.addObserver(self, selector: #selector(iapProductsFailed), name: ServerNotifications.iapProductsFailed, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(subscriptionStatusChanged), name: ServerNotifications.subscriptionStatusChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: Constants.Notifications.themeChanged, object: nil)

tableView.tableHeaderView = updatedHeaderContentView
tableView.tableHeaderView = headerStackView
tableView.widthAnchor.constraint(equalTo: headerStackView.widthAnchor).isActive = true
receiveGravatarViewModelUpdates()
}

override func viewWillAppear(_ animated: Bool) {
Expand Down Expand Up @@ -91,7 +160,7 @@ class AccountViewController: UIViewController, ChangeEmailDelegate {

private func updateDisplayedData() {
headerViewModel.update()

toggleGravatarProfileView()
// Show the upsell if the users subscription is expiring in the next 30 days
let isExpiring = (SubscriptionHelper.timeToSubscriptionExpiry() ?? .infinity) < Constants.Limits.maxSubscriptionExpirySeconds

Expand Down Expand Up @@ -173,3 +242,120 @@ class AccountViewController: UIViewController, ChangeEmailDelegate {
}
}
}

extension AccountViewController: ProfileViewDelegate {

private func receiveGravatarViewModelUpdates() {
gravatarViewModel.$profileFetchingResult.sink { [weak self] result in
guard let self else { return }
guard let result else {
var newConfig = self.gravatarConfiguration.updatedForPocketCasts(delegate: self)
newConfig.model = nil
newConfig.summaryModel = nil
self.gravatarConfiguration = newConfig
return
}

switch result {
case .success(let profile):
var newConfig = self.gravatarConfiguration.updatedForPocketCasts(delegate: self)
newConfig.model = profile
newConfig.summaryModel = profile
self.gravatarConfiguration = newConfig
case .failure(Gravatar.APIError.responseError(reason: let reason)) where reason.httpStatusCode == 404:
// No Gravatar profile found, switch to the "claim profile" state.
var claimProfileConfig = ProfileViewConfiguration.claimProfile(profileStyle: gravatarConfiguration.profileStyle)
claimProfileConfig.padding = UIConstants.Gravatar.padding
claimProfileConfig.delegate = self
claimProfileConfig.palette = .custom(Palette.pocketCasts)
self.gravatarConfiguration = claimProfileConfig
case .failure:
// TODO: handle error
break
}
}.store(in: &cancellables)

gravatarViewModel.$isLoading.sink { [weak self] isLoading in
guard let self else { return }
var newConfig = self.gravatarConfiguration
newConfig.isLoading = isLoading
self.gravatarConfiguration = newConfig
}.store(in: &cancellables)
}

public func clearGravatarProfile() {
gravatarViewModel.clear()
}

public func fetchGravatarProfile() {
guard headerViewModel.shouldDisplayGravatarProfile, let email = headerViewModel.profile.email else { return }
Task {
await gravatarViewModel.fetchProfile(profileIdentifier: ProfileIdentifier.email(email))
}
}

func profileView(_ view: GravatarUI.BaseProfileView, didTapOnProfileButtonWithStyle style: GravatarUI.ProfileButtonStyle, profileURL: URL?) {
guard let profileURL else { return }
let safari = SFSafariViewController(url: profileURL)
present(safari, animated: true)
}

func profileView(_ view: GravatarUI.BaseProfileView, didTapOnAccountButtonWithModel accountModel: any GravatarUI.AccountModel) {
guard let accountURL = accountModel.accountURL else { return }
let safari = SFSafariViewController(url: accountURL)
present(safari, animated: true)
}

func profileView(_ view: GravatarUI.BaseProfileView, didTapOnAvatarWithID avatarID: Gravatar.AvatarIdentifier?) {}
}

extension AccountViewController: AvatarProviding {
var avatarView: UIView {
subscriptionAvatarView
}

func setImage(with source: URL?, placeholder: UIImage?, options: [GravatarUI.ImageSettingOption]?) async throws {
// no need
}

func setImage(_ image: UIImage?) {
// no need
}

func refresh(with paletteType: GravatarUI.PaletteType) {
// no need
}
}

fileprivate extension ProfileViewConfiguration {

func updatedForPocketCasts(delegate: ProfileViewDelegate) -> ProfileViewConfiguration {
var config = self
config.padding = AccountViewController.UIConstants.Gravatar.padding
config.profileButtonStyle = .edit
config.delegate = delegate
config.palette = .custom(Palette.pocketCasts)
return config
}
}

extension Palette {
static func pocketCasts() -> GravatarUI.Palette {
GravatarUI.Palette(
name: Theme.sharedTheme.activeTheme.description,
foreground: ForegroundColors(primary: ThemeColor.primaryText01(),
primarySlightlyDimmed: ThemeColor.secondaryText01(),
secondary: ThemeColor.primaryText02()),
background: BackgroundColors(primary: ThemeColor.primaryUi01()),
avatar: AvatarColors(border: ThemeColor.primaryUi05(),
background: ThemeColor.primaryUi04()),
border: ThemeColor.primaryUi05(),
placeholder: PlaceholderColors(backgroundColor: ThemeColor.primaryText01().withAlphaComponent(0.05),
loadingAnimationColors: [
ThemeColor.primaryText01().withAlphaComponent(0.05),
ThemeColor.primaryText01().withAlphaComponent(0.1)
]),
preferredUserInterfaceStyle: Theme.sharedTheme.activeTheme.isDark ? .dark: .light
)
}
}
8 changes: 5 additions & 3 deletions podcasts/Profile - SwiftUI/Account/AccountHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ struct AccountHeaderView: View {
var body: some View {
container { proxy in
VStack(spacing: Constants.padding.vertical) {
SubscriptionProfileImage(viewModel: viewModel)
.frame(width: Constants.imageSize, height: Constants.imageSize)
if !viewModel.shouldDisplayGravatarProfile {
SubscriptionProfileImage(viewModel: viewModel)
.frame(width: Constants.imageSize, height: Constants.imageSize)
}
ProfileInfoLabels(profile: viewModel.profile, alignment: .center, spacing: Constants.spacing)
// Subscription badge
viewModel.subscription.map {
Expand Down Expand Up @@ -107,7 +109,7 @@ struct AccountHeaderView: View {
content(proxy)
.frame(maxWidth: .infinity)
}
.padding(.top, Constants.padding.top)
.padding(.top, viewModel.shouldDisplayGravatarProfile ? Constants.padding.vertical : Constants.padding.top)
.padding(.bottom, Constants.padding.bottom)
.padding(.horizontal, Constants.padding.horizontal)
} contentSizeUpdated: { size in
Expand Down
Loading