Skip to content

Commit

Permalink
Add ability to donate to Mastodon from the app (#1353)
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.

Amounts will not necessarily be returned from the server in order. The first amount returned is taken as the default and the amounts are sorted before display.

---------

Co-authored-by: Marcus Kida <[email protected]>
  • Loading branch information
whattherestimefor and kimar authored Nov 7, 2024
1 parent fdca501 commit 9d774cb
Show file tree
Hide file tree
Showing 41 changed files with 1,862 additions and 40 deletions.
22 changes: 21 additions & 1 deletion Localization/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@
"general": "General",
"notifications": "Notifications",
"privacy_safety": "Privacy & Safety",
"support_mastodon": "Support Mastodon",
"support_mastodon": "Donate to Mastodon",
"about_mastodon": "About Mastodon",
"server_details": "Server Details",
"logout": "Logout %@"
Expand Down Expand Up @@ -880,6 +880,10 @@
"show_followers_and_following": "Show Followers & Following",
"suggest_my_account_to_others": "Suggest My Account to Others",
"appear_in_search_engines": "Appear in Search Engines"
},
"donation": {
"title": "Donate to Mastodon",
"manage": "Manage donations"
}
},
"report": {
Expand Down Expand Up @@ -965,6 +969,22 @@
"follow": "Follow",
"unfollow": "Unfollow"
}
},
"donation": {
"picker": {
"once_title": "Just once",
"monthly_title": "Monthly",
"yearly_title": "Yearly"
},
"donatebuttontitle": "Donate",
"currency": "Currency",
"success": {
"title": "Thank you for your contribution!",
"subtitle": "You should receive an email confirming your donation soon.",
"server_error_title": "Payment failed",
"server_error_message": "We are sorry, an error occurred and we have not been able to process your donation.\n\nPlease retry in a few minutes.",
"share_button_title": "Spread the word"
}
}
},
"extension": {
Expand Down
36 changes: 36 additions & 0 deletions Mastodon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
2A631AE82B8C9F6600FE0778 /* LanguagePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A631AE72B8C9F6600FE0778 /* LanguagePickerViewController.swift */; };
2A64515E29642A8A00CD8B8A /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */; };
2A64516929642A8B00CD8B8A /* OpenInActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
2A64A2912C92EF0C00E5E913 /* HomeTimelineViewModel+Donation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A64A2902C92EF0C00E5E913 /* HomeTimelineViewModel+Donation.swift */; };
2A64A2942C92F71500E5E913 /* DonationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A64A2932C92F71500E5E913 /* DonationViewController.swift */; };
2A64A2962C92FD5700E5E913 /* DonationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A64A2952C92FD5700E5E913 /* DonationBanner.swift */; };
2A71F541296DBDA80049F54A /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A71F53D296DBDA80049F54A /* Media.xcassets */; };
2A71F542296DBDA80049F54A /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = 2A71F53E296DBDA80049F54A /* Action.js */; };
2A71F543296DBDA80049F54A /* ActionRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */; };
Expand Down Expand Up @@ -509,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 @@ -660,6 +667,9 @@
2A631AE72B8C9F6600FE0778 /* LanguagePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePickerViewController.swift; sourceTree = "<group>"; };
2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
2A64A2902C92EF0C00E5E913 /* HomeTimelineViewModel+Donation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Donation.swift"; sourceTree = "<group>"; };
2A64A2932C92F71500E5E913 /* DonationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationViewController.swift; sourceTree = "<group>"; };
2A64A2952C92FD5700E5E913 /* DonationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationBanner.swift; sourceTree = "<group>"; };
2A71F53D296DBDA80049F54A /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
2A71F53E296DBDA80049F54A /* Action.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = Action.js; sourceTree = "<group>"; };
2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionRequestHandler.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1257,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 @@ -1436,6 +1450,19 @@
path = Language;
sourceTree = "<group>";
};
2A64A2922C92F70700E5E913 /* Donation */ = {
isa = PBXGroup;
children = (
2A64A2952C92FD5700E5E913 /* DonationBanner.swift */,
FBD689B42CCBF09F00CE29F3 /* DonationCampaignViewModel.swift */,
2A64A2932C92F71500E5E913 /* DonationViewController.swift */,
FB7C4CC52CD2CAA800F6129A /* DonationCompletionViewController.swift */,
FB7C4CCB2CD55DEB00F6129A /* NavigationFlow.swift */,
FB7C4CCD2CD55DFE00F6129A /* NewDonationNavigationFlow.swift */,
);
path = Donation;
sourceTree = "<group>";
};
2A71F53C296DBDA80049F54A /* OpenInActionExtension */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1556,6 +1583,7 @@
2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */,
2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */,
2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */,
2A64A2902C92EF0C00E5E913 /* HomeTimelineViewModel+Donation.swift */,
D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */,
);
path = HomeTimeline;
Expand Down Expand Up @@ -2578,6 +2606,7 @@
DB8AF55525C1379F002E6C99 /* Scene */ = {
isa = PBXGroup;
children = (
2A64A2922C92F70700E5E913 /* Donation */,
2D7631A425C1532200929FB9 /* Share */,
DB6180E426391A500018D199 /* Transition */,
DB852D1D26FB021900FC9D81 /* Root */,
Expand Down Expand Up @@ -3545,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 All @@ -3563,6 +3594,7 @@
DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */,
62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */,
DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */,
2A64A2942C92F71500E5E913 /* DonationViewController.swift in Sources */,
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,
DB5B54AB2833C12A00DEF8B2 /* RebloggedByViewController.swift in Sources */,
D81D12462A4E1861005009D4 /* PolicySelectionViewController.swift in Sources */,
Expand Down Expand Up @@ -3603,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 @@ -3654,6 +3687,7 @@
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
DB5B729E273113F300081888 /* FollowingListViewModel+State.swift in Sources */,
D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */,
2A64A2912C92EF0C00E5E913 /* HomeTimelineViewModel+Donation.swift in Sources */,
DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */,
DB63F76227996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift in Sources */,
DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */,
Expand Down Expand Up @@ -3815,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 Expand Up @@ -3893,6 +3928,7 @@
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
DBEFCD7D282A2A3B00C0ABEA /* ReportServerRulesViewController.swift in Sources */,
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
2A64A2962C92FD5700E5E913 /* DonationBanner.swift in Sources */,
D8F917122A4C6B67008A5370 /* GeneralSettingsViewController.swift in Sources */,
D81A94122B07A1BE0067A19D /* ProfileCardTableViewCell.swift in Sources */,
DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.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
125 changes: 125 additions & 0 deletions Mastodon/Scene/Donation/DonationBanner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright © 2024 Mastodon gGmbH. All rights reserved.

import MastodonAsset
import MastodonSDK
import UIKit

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

public private(set) var campaign: Mastodon.Entity.DonationCampaign?
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(_:)))
gestureRecognizer.numberOfTapsRequired = 1
return gestureRecognizer
}()

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

var onClose: ((String?) -> 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 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)
attributedString.addAttributes(
[
.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)),
.underlineStyle: NSUnderlineStyle.single.rawValue,
.underlineColor: 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
)
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),
closeButton.topAnchor.constraint(equalTo: topAnchor),
closeButton.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}

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

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

0 comments on commit 9d774cb

Please sign in to comment.