Skip to content

Commit

Permalink
Add ability to donate to Mastodon from the app
Browse files Browse the repository at this point in the history
This change only affects users logged in to mastodon.social or mastodon.online. A banner may be periodically displayed at the bottom of the homescreen encouraging donations and menu options are now available in Settings to make new donations or manage existing ones.

Applies swift-format to added files.
  • Loading branch information
whattherestimefor committed Nov 5, 2024
1 parent ceecabe commit 9fe9672
Show file tree
Hide file tree
Showing 36 changed files with 1,264 additions and 200 deletions.
1 change: 1 addition & 0 deletions Localization/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,7 @@
"monthly_title": "Monthly",
"yearly_title": "Yearly"
},
"donatebuttontitle": "Donate",
"currency": "Currency",
"success": {
"title": "Thank you for your contribution!",
Expand Down
18 changes: 17 additions & 1 deletion Mastodon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,10 @@
DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEEC98279BDCDE004F81DD /* ProfileAboutViewModel.swift */; };
DBFEEC9B279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEEC9A279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift */; };
DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEEC9C279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift */; };
FB7C4CC62CD2CAB000F6129A /* DonationCompletionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7C4CC52CD2CAA800F6129A /* DonationCompletionViewController.swift */; };
FB7C4CCC2CD55DEB00F6129A /* NavigationFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7C4CCB2CD55DEB00F6129A /* NavigationFlow.swift */; };
FB7C4CCE2CD55DFF00F6129A /* NewDonationNavigationFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7C4CCD2CD55DFE00F6129A /* NewDonationNavigationFlow.swift */; };
FBD689B52CCBF0AC00CE29F3 /* DonationCampaignViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD689B42CCBF09F00CE29F3 /* DonationCampaignViewModel.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -1263,6 +1267,10 @@
DBFEF06726A58D07006D7ED1 /* ShareActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareActionExtension.entitlements; sourceTree = "<group>"; };
E9AABD3D26B64B8C00E237DA /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Intents.strings; sourceTree = "<group>"; };
E9AABD4026B64B8D00E237DA /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
FB7C4CC52CD2CAA800F6129A /* DonationCompletionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationCompletionViewController.swift; sourceTree = "<group>"; };
FB7C4CCB2CD55DEB00F6129A /* NavigationFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationFlow.swift; sourceTree = "<group>"; };
FB7C4CCD2CD55DFE00F6129A /* NewDonationNavigationFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDonationNavigationFlow.swift; sourceTree = "<group>"; };
FBD689B42CCBF09F00CE29F3 /* DonationCampaignViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationCampaignViewModel.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -1445,8 +1453,12 @@
2A64A2922C92F70700E5E913 /* Donation */ = {
isa = PBXGroup;
children = (
2A64A2932C92F71500E5E913 /* DonationViewController.swift */,
2A64A2952C92FD5700E5E913 /* DonationBanner.swift */,
FBD689B42CCBF09F00CE29F3 /* DonationCampaignViewModel.swift */,
2A64A2932C92F71500E5E913 /* DonationViewController.swift */,
FB7C4CC52CD2CAA800F6129A /* DonationCompletionViewController.swift */,
FB7C4CCB2CD55DEB00F6129A /* NavigationFlow.swift */,
FB7C4CCD2CD55DFE00F6129A /* NewDonationNavigationFlow.swift */,
);
path = Donation;
sourceTree = "<group>";
Expand Down Expand Up @@ -3562,6 +3574,8 @@
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
DBEFCD80282A2AA900C0ABEA /* ReportServerRulesViewModel.swift in Sources */,
FB7C4CCE2CD55DFF00F6129A /* NewDonationNavigationFlow.swift in Sources */,
FB7C4CCC2CD55DEB00F6129A /* NavigationFlow.swift in Sources */,
DB0617FF27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift in Sources */,
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */,
DB0FCB982797F6BF006C02E2 /* UserTableViewCell+ViewModel.swift in Sources */,
Expand Down Expand Up @@ -3621,6 +3635,7 @@
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */,
DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */,
FBD689B52CCBF0AC00CE29F3 /* DonationCampaignViewModel.swift in Sources */,
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */,
DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */,
DB3E6FEC2806D7F100B035AE /* DiscoveryNewsViewController.swift in Sources */,
Expand Down Expand Up @@ -3834,6 +3849,7 @@
2A0BF97F2C0622AA004A1E29 /* PrivacySafetyViewController.swift in Sources */,
DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */,
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
FB7C4CC62CD2CAB000F6129A /* DonationCompletionViewController.swift in Sources */,
DB0FCB822796AC78006C02E2 /* UserTimelineViewController+DataSourceProvider.swift in Sources */,
DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */,
D85DF96D2C481AF700A01408 /* NotificationPolicyHeaderView.swift in Sources */,
Expand Down
3 changes: 1 addition & 2 deletions Mastodon/Coordinator/SceneCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,7 @@ private extension SceneCoordinator {
_viewController.viewModel = viewModel
viewController = _viewController
case .mastodonWebView(let viewModel):
let _viewController = WebViewController()
_viewController.viewModel = viewModel
let _viewController = WebViewController(viewModel)
viewController = _viewController
case .searchDetail(let viewModel):
let _viewController = SearchDetailViewController(appContext: appContext, sceneCoordinator: self, authContext: viewModel.authContext)
Expand Down
79 changes: 49 additions & 30 deletions Mastodon/Scene/Donation/DonationBanner.swift
Original file line number Diff line number Diff line change
@@ -1,103 +1,122 @@
// Copyright © 2024 Mastodon gGmbH. All rights reserved.

import UIKit
import MastodonSDK
import MastodonAsset
import MastodonSDK
import UIKit

class DonationBanner: UIView {
private enum Constants {
static let padding: CGFloat = 16
static let textToButtonPadding: CGFloat = 48
}

private var campaign: Mastodon.Entity.DonationCampaign?
private lazy var backgroundImageView = UIImageView(image: Asset.Asset.scribble.image)
private lazy var backgroundImageView = UIImageView(
image: Asset.Asset.scribble.image)
private let messageLabel = UILabel()
private lazy var closeButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(.init(systemName: "xmark"), for: .normal)
return button
}()
private lazy var tapGestureRecognizer: UITapGestureRecognizer = {
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showDonationDialogPressed(_:)))
let gestureRecognizer = UITapGestureRecognizer(
target: self, action: #selector(showDonationDialogPressed(_:)))
gestureRecognizer.numberOfTapsRequired = 1
return gestureRecognizer
}()

init() {
super.init(frame: .zero)
self.overrideUserInterfaceStyle = .dark
setupViews()
}

var onClose: (() -> Void)?
var onShowDonationDialog: ((Mastodon.Entity.DonationCampaign) -> Void)?

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func update(campaign: Mastodon.Entity.DonationCampaign) {
self.campaign = campaign
let spacing = " "
let stringValue = "\(campaign.bannerMessage)\(spacing)\(campaign.bannerButtonText)"
let stringValue =
"\(campaign.bannerMessage)\(spacing)\(campaign.bannerButtonText)"
let attributedString = NSMutableAttributedString(string: stringValue)
let fullTextRange = NSRange(location: 0, length: stringValue.length)
let buttonRange = NSRange(location: campaign.bannerMessage.length + spacing.length, length: campaign.bannerButtonText.length)
let buttonRange = NSRange(
location: campaign.bannerMessage.length + spacing.length,
length: campaign.bannerButtonText.length)
attributedString.addAttributes(
[
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14, weight: .regular)),
.foregroundColor: Asset.Colors.Secondary.onContainer.color
.font: UIFontMetrics(forTextStyle: .body).scaledFont(
for: .systemFont(ofSize: 14, weight: .regular)),
.foregroundColor: Asset.Colors.Secondary.onContainer.color,
],
range: fullTextRange
)
attributedString.addAttributes(
[
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14, weight: .bold)),
.font: UIFontMetrics(forTextStyle: .body).scaledFont(
for: .systemFont(ofSize: 14, weight: .bold)),
.underlineStyle: NSUnderlineStyle.single.rawValue,
.underlineColor: Asset.Colors.goldenrod.color,
.foregroundColor: Asset.Colors.goldenrod.color
.foregroundColor: Asset.Colors.goldenrod.color,
],
range: buttonRange
)
messageLabel.attributedText = attributedString
}

private func setupViews() {
addGestureRecognizer(tapGestureRecognizer)
backgroundColor = Asset.Colors.Secondary.container.color
addSubview(backgroundImageView)
backgroundImageView.translatesAutoresizingMaskIntoConstraints = false
backgroundImageView.alpha = 0.08

closeButton.tintColor = Asset.Colors.Secondary.onContainer.color

messageLabel.translatesAutoresizingMaskIntoConstraints = false
messageLabel.numberOfLines = 0

closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.setContentCompressionResistancePriority(.required, for: .horizontal)
closeButton.addTarget(self, action: #selector(closeButtonPressed(_:)), for: .touchUpInside)
closeButton.setContentCompressionResistancePriority(
.required, for: .horizontal)
closeButton.addTarget(
self, action: #selector(closeButtonPressed(_:)), for: .touchUpInside
)
addSubview(messageLabel)
addSubview(closeButton)

NSLayoutConstraint.activate([
backgroundImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
backgroundImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
messageLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.padding),
messageLabel.topAnchor.constraint(equalTo: topAnchor, constant: Constants.padding),
messageLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.padding),
messageLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -Constants.padding*2),
closeButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.padding/2),
backgroundImageView.trailingAnchor.constraint(
equalTo: trailingAnchor),
backgroundImageView.centerYAnchor.constraint(
equalTo: centerYAnchor),
messageLabel.leadingAnchor.constraint(
equalTo: leadingAnchor, constant: Constants.padding),
messageLabel.topAnchor.constraint(
equalTo: topAnchor, constant: Constants.padding),
messageLabel.bottomAnchor.constraint(
equalTo: bottomAnchor, constant: -Constants.padding),
messageLabel.trailingAnchor.constraint(
equalTo: closeButton.leadingAnchor,
constant: -Constants.padding * 2),
closeButton.trailingAnchor.constraint(
equalTo: trailingAnchor, constant: -Constants.padding / 2),
closeButton.topAnchor.constraint(equalTo: topAnchor),
closeButton.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}

@objc
private func closeButtonPressed(_ sender: Any?) {
onClose?()
}

@objc
private func showDonationDialogPressed(_ sender: Any?) {
guard let campaign else { return }
Expand Down
Loading

0 comments on commit 9fe9672

Please sign in to comment.