[Payments] Biometry Payments Lock

* payment lock

* add 30 day timeout to showing the payment lock suggestion

* fix linting

* update comment

* Payment Lock: Fix project file & submodules (#4878)

* revert ringrtc changes

* revert pods changes

* fix project file

* PR Comment suggestions and fixes

* PR suggestions

* revert screen lock to previous implementation

* rename OWSPaymentLock to OWSPaymentsLock (missing plural s), remove
super-class dependency. Make OWSPaymentsLock more "swifty".

* revert changes to call-site for OWSScreenLock

* update call-sites for OWSPaymentsLock

* forgot to add PaymentOnboarding class and localization comment improvements

* change function call-site

* Remove OWSLocalAuthentication from project

* remove OWSLocalAuth from project file

* make edge-case default more secure if storage not ready

* fix project file

* lint fix

* fix linting errors

* Revert compact format style for OWSLocalizedString

* fix localization call-sites to use literals, update localization file after autogen

* Add new Payments Lock Prompt view to be shown after activating payments. Factor out the BiometryType "current biometry" into its own class with helpers to easily getting the devices current setup.

* fix linting issues, sort project file, and autogen localizations

* require payments lock to look at the recovery phrase

* fix linting issue

* fix missing localizations (caused by dynamic creation in previous commit)

* use Pods commit from signal/main

* re-run linter on latest commits

* new header comments for new branch files

* update submodule commits

* Use existing secondsInDay constant kDayInterval, use weak self guard statement instead of optional self in escaping closure.

* Revert submodule changes

* Remove duplicate copyright headers

* Revert copyright header changes

* Restore some missing translations

* Add localization for unknown LocalAuthentication error/state.

* remove Pods changes

* linting

* use submodule commits from main

* subclass the old Objc OWSViewController super-class, should fix CICD

* change capitalized Passcode to lower-case in non-title situation.

* remove early exit guard from biometryType computed function. Reason being that the type can be gathered from a policy that evaluates to false. In a case where the user has a FaceID phone but its disabled, the messaging would be incorrect. removing the early exit evaluates the biometryType which apple provides even if the policy returns false.

* inline snooze date

* inject write transaction to some convenience methods to reduce database overhead when multiple calls need to happen at the same time.

* make combined set and snooze function, update combined call-site

* rename long function

* use false instead of sender.on to avoid sneaky view issues

* add tryToUnlockPromise function to clean up call sites

* change superclass, fix Promise statement

* call super class function

* remove objc

* add unlock failed action sheet helper class and put at all relevant call-sites, still need to test though.

* add unlock failed action sheet helper class and put at all relevant call-sites, still need to test though. linting

* project file changes for new file

* re-render payments lock toggle after failure to change setting.

re-render payments lock toggle after failure to change setting.

* move around action sheet call-site to account for an action sheet already being presented. remove unecc. return

* fix merge conflicts, linting

* re-gen strings file

---------

Co-authored-by: Max Radermacher <max@signal.org>
This commit is contained in:
Adam Mork 2023-02-03 12:56:54 -08:00 committed by GitHub
parent 2f384bf7a9
commit 728862da6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1035 additions and 23 deletions

View File

@ -753,6 +753,7 @@
5011D1CB293FC7E000064098 /* DomainFrontingCountryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5011D1CA293FC7E000064098 /* DomainFrontingCountryViewController.swift */; };
5011D1CD29400E7300064098 /* DeviceProvisioningURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5011D1CC29400E7300064098 /* DeviceProvisioningURL.swift */; };
50169695291B0627007AD709 /* ContactDiscoveryManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50169694291B0627007AD709 /* ContactDiscoveryManagerTest.swift */; };
501D64FC28C027BA008D5993 /* OWSPaymentsLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501D64FA28C027BA008D5993 /* OWSPaymentsLock.swift */; };
502B1B55297B28AF00FDB3AE /* ErrorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B1B54297B28AF00FDB3AE /* ErrorTest.swift */; };
503614CF282AF657008128B4 /* GiftBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503614CE282AF657008128B4 /* GiftBadgeView.swift */; };
503BDDB4296F3E2C00FED3B2 /* SystemContactsDataProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503BDDB3296F3E2C00FED3B2 /* SystemContactsDataProviderTest.swift */; };
@ -805,6 +806,7 @@
667E90D028E799D1005FE603 /* MyStorySettingsLearnMoreSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667E90CF28E799D1005FE603 /* MyStorySettingsLearnMoreSheetViewController.swift */; };
667EDE6428F8D6B7001FB487 /* YYAnimatedImage+Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667EDE6328F8D6B7001FB487 /* YYAnimatedImage+Duration.swift */; };
667EDE6628FA0372001FB487 /* StoryBadgeCountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667EDE6528FA0372001FB487 /* StoryBadgeCountManager.swift */; };
6688E602298232A4004467C8 /* PaymentActionSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6688E601298232A4004467C8 /* PaymentActionSheets.swift */; };
668CAB3E289983520085A2C3 /* AudioMessagePlaybackRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668CAB3D289983520085A2C3 /* AudioMessagePlaybackRateView.swift */; };
668FE09B28B923A4008B9071 /* Bool+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668FE09A28B923A4008B9071 /* Bool+SSK.swift */; };
668FE09F28B947ED008B9071 /* StoryContextMenuGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668FE09E28B947ED008B9071 /* StoryContextMenuGenerator.swift */; };
@ -830,8 +832,11 @@
66AF4D7328D1377E008A156E /* SignalAttachment+VideoSegmenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66AF4D7228D1377E008A156E /* SignalAttachment+VideoSegmenting.swift */; };
66B8B28028C94C0F005EAFE0 /* DelegatingContextMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B8B27F28C94C0F005EAFE0 /* DelegatingContextMenuButton.swift */; };
66BE544D28CA4EC10021AFF1 /* StoryContextOnboardingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BE544C28CA4EC10021AFF1 /* StoryContextOnboardingOverlayView.swift */; };
66CE755F28C332AF00D5FA79 /* PaymentOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CE755E28C332AF00D5FA79 /* PaymentOnboarding.swift */; };
66D709E928E3999400B5013A /* StoryContextAssociatedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D709E828E3999400B5013A /* StoryContextAssociatedData.swift */; };
66F44B4B2909EEDA004CF66C /* OWSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F44B4A2909EEDA004CF66C /* OWSViewController.swift */; };
66FA2B1D28CB0DE1006845CD /* PaymentsBiometryLockPromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FA2B1C28CB0DE1006845CD /* PaymentsBiometryLockPromptViewController.swift */; };
66FA2B1F28CBA4A5006845CD /* BiometryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FA2B1E28CBA4A5006845CD /* BiometryType.swift */; };
66FBC4E128DA820900BD9E8B /* MyStorySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FBC4E028DA820900BD9E8B /* MyStorySettingsViewController.swift */; };
66FBC4E328DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FBC4E228DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift */; };
760981882936DE90008F8300 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 760981872936DE90008F8300 /* BezierPathView.swift */; };
@ -3118,6 +3123,7 @@
5011D1CA293FC7E000064098 /* DomainFrontingCountryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainFrontingCountryViewController.swift; sourceTree = "<group>"; };
5011D1CC29400E7300064098 /* DeviceProvisioningURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProvisioningURL.swift; sourceTree = "<group>"; };
50169694291B0627007AD709 /* ContactDiscoveryManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDiscoveryManagerTest.swift; sourceTree = "<group>"; };
501D64FA28C027BA008D5993 /* OWSPaymentsLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSPaymentsLock.swift; sourceTree = "<group>"; };
502B1B54297B28AF00FDB3AE /* ErrorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorTest.swift; sourceTree = "<group>"; };
503614CE282AF657008128B4 /* GiftBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftBadgeView.swift; sourceTree = "<group>"; };
503614D0282C5703008128B4 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = translations/ro.lproj/PluralAware.stringsdict; sourceTree = "<group>"; };
@ -3175,6 +3181,7 @@
667E90CF28E799D1005FE603 /* MyStorySettingsLearnMoreSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyStorySettingsLearnMoreSheetViewController.swift; sourceTree = "<group>"; };
667EDE6328F8D6B7001FB487 /* YYAnimatedImage+Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "YYAnimatedImage+Duration.swift"; sourceTree = "<group>"; };
667EDE6528FA0372001FB487 /* StoryBadgeCountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryBadgeCountManager.swift; sourceTree = "<group>"; };
6688E601298232A4004467C8 /* PaymentActionSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentActionSheets.swift; sourceTree = "<group>"; };
668AB0CB28AD610600B31984 /* StoryUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryUtil.swift; sourceTree = "<group>"; };
668CAB3D289983520085A2C3 /* AudioMessagePlaybackRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessagePlaybackRateView.swift; sourceTree = "<group>"; };
668FE09A28B923A4008B9071 /* Bool+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bool+SSK.swift"; sourceTree = "<group>"; };
@ -3200,8 +3207,11 @@
66AF4D7228D1377E008A156E /* SignalAttachment+VideoSegmenting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SignalAttachment+VideoSegmenting.swift"; sourceTree = "<group>"; };
66B8B27F28C94C0F005EAFE0 /* DelegatingContextMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingContextMenuButton.swift; sourceTree = "<group>"; };
66BE544C28CA4EC10021AFF1 /* StoryContextOnboardingOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryContextOnboardingOverlayView.swift; sourceTree = "<group>"; };
66CE755E28C332AF00D5FA79 /* PaymentOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentOnboarding.swift; sourceTree = "<group>"; };
66D709E828E3999400B5013A /* StoryContextAssociatedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryContextAssociatedData.swift; sourceTree = "<group>"; };
66F44B4A2909EEDA004CF66C /* OWSViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSViewController.swift; sourceTree = "<group>"; };
66FA2B1C28CB0DE1006845CD /* PaymentsBiometryLockPromptViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsBiometryLockPromptViewController.swift; sourceTree = "<group>"; };
66FA2B1E28CBA4A5006845CD /* BiometryType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometryType.swift; sourceTree = "<group>"; };
66FBC4E028DA820900BD9E8B /* MyStorySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyStorySettingsViewController.swift; sourceTree = "<group>"; };
66FBC4E228DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectMyStoryRecipientsViewController.swift; sourceTree = "<group>"; };
70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
@ -4984,6 +4994,7 @@
179E8C30276A711100AF640F /* AFQueryString.m */,
6675F64C2925C012007A311E /* APNSRotationStore.swift */,
349ED991221EE80D008045B0 /* AppPreferences.swift */,
66FA2B1E28CBA4A5006845CD /* BiometryType.swift */,
88D7BA9D266809F50088D1C2 /* CallMessageRelay.swift */,
34A955AB271B521500B05242 /* CommonStrings.swift */,
3464451022B7F97100A957B1 /* DateUtil.h */,
@ -4998,6 +5009,7 @@
3464450C22B7F93600A957B1 /* OWSOrphanDataCleaner.h */,
3464450B22B7F93600A957B1 /* OWSOrphanDataCleaner.m */,
F9CC66C02937B71E002172D0 /* OWSOrphanDataCleaner.swift */,
501D64FA28C027BA008D5993 /* OWSPaymentsLock.swift */,
346129371FD1B47200532771 /* OWSPreferences.h */,
346129381FD1B47200532771 /* OWSPreferences.m */,
34641E172088D7E900E2EDE5 /* OWSScreenLock.swift */,
@ -5256,6 +5268,7 @@
isa = PBXGroup;
children = (
34FB6A5425D2E17200E599B1 /* PaymentModelCell.swift */,
66FA2B1C28CB0DE1006845CD /* PaymentsBiometryLockPromptViewController.swift */,
347030C525F66C24006C3BF5 /* PaymentsDeactivateViewController.swift */,
34FB6A4E25D1C6AC00E599B1 /* PaymentsDetailViewController.swift */,
3498AC8E2518E92B00B1F315 /* PaymentsHistory.swift */,
@ -5457,6 +5470,8 @@
34A9554E271B510400B05242 /* OWSStackView.swift */,
F9C8CFCF293580D00094469C /* OWSTextField.swift */,
760981892936EC8D008F8300 /* OWSTextView.swift */,
6688E601298232A4004467C8 /* PaymentActionSheets.swift */,
66CE755E28C332AF00D5FA79 /* PaymentOnboarding.swift */,
32FAB9292727A57100FB76A6 /* PrimaryImageView.swift */,
45A6DAD51EBBF85500893231 /* ReminderView.swift */,
34A95531271B510400B05242 /* ResizingScrollView.swift */,
@ -10217,6 +10232,8 @@
66F44B4B2909EEDA004CF66C /* OWSViewController.swift in Sources */,
3402AA59271D9DCD0084CBAE /* OWSViewControllerObjc.m in Sources */,
3402AA3B271D9DCD0084CBAE /* OWSWindow.swift in Sources */,
6688E602298232A4004467C8 /* PaymentActionSheets.swift in Sources */,
66CE755F28C332AF00D5FA79 /* PaymentOnboarding.swift in Sources */,
3465F4DD2728812B001663AF /* Payments.swift in Sources */,
34A955B9271B553D00B05242 /* PaymentsFormat.swift in Sources */,
3465F4D827287677001663AF /* PaymentsImpl.swift in Sources */,
@ -10326,6 +10343,7 @@
347850691FD9B78A007B8332 /* AppSetup.m in Sources */,
34FC7EEC265834F30046707A /* AvatarBuilder.swift in Sources */,
883A7FD2269F642F00841DF9 /* AvatarModel.swift in Sources */,
66FA2B1F28CBA4A5006845CD /* BiometryType.swift in Sources */,
88F15F9A25AD4AE0008ABD47 /* BroadcastMediaMessageJob.swift in Sources */,
500FE490288615BA00FA090C /* CachedBadge.swift in Sources */,
88D7BA9E266809F50088D1C2 /* CallMessageRelay.swift in Sources */,
@ -10365,6 +10383,7 @@
4C046AA7236148880035B234 /* OWSGroupSyncProcessingJobQueue.swift in Sources */,
3464450D22B7F93600A957B1 /* OWSOrphanDataCleaner.m in Sources */,
F9CC66C12937B71E002172D0 /* OWSOrphanDataCleaner.swift in Sources */,
501D64FC28C027BA008D5993 /* OWSPaymentsLock.swift in Sources */,
3461293A1FD1B47300532771 /* OWSPreferences.m in Sources */,
346129B51FD1F7E800532771 /* OWSProfileManager.m in Sources */,
3470249E2385B6360078D72C /* OWSProfileManager.swift in Sources */,
@ -10860,6 +10879,7 @@
4579431E1E7C8CE9008ED0C0 /* Pastelog.m in Sources */,
34067EAB2710D61A000407C3 /* Pastelog.swift in Sources */,
34FB6A5525D2E17200E599B1 /* PaymentModelCell.swift in Sources */,
66FA2B1D28CB0DE1006845CD /* PaymentsBiometryLockPromptViewController.swift in Sources */,
347030C625F66C24006C3BF5 /* PaymentsDeactivateViewController.swift in Sources */,
34FB6A4F25D1C6AC00E599B1 /* PaymentsDetailViewController.swift in Sources */,
3498AC912518E92B00B1F315 /* PaymentsHistory.swift in Sources */,

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "payments-lock.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 88 88"><defs><style>.cls-1{fill:#e9e9e9;}</style></defs><circle class="cls-1" cx="44" cy="44" r="44"/><path d="M55.7305489,39.6992862v-6.5743008c0-3.1111002-1.2359009-6.0948-3.4357986-8.2946796-2.1999016-2.1999002-5.1836014-3.4357905-8.2947006-3.4357905s-6.0948009,1.2358904-8.2947006,3.4357905c-2.1998997,2.1998796-3.4357986,5.1835794-3.4357986,8.2946796v6.5743008c-1.1154003,.1561985-2.1373005,.7089996-2.8784008,1.5570984-.7411003,.8481007-1.1518002,1.9349003-1.1569996,3.0612011v17.6030006c.0038996,1.2412987,.4986992,2.4306984,1.3764,3.3084984,.8778,.8778,2.0671997,1.3726006,3.3085995,1.3764h22.1617985c1.2413025-.0037994,2.4307022-.4986,3.3085022-1.3764s1.3726006-2.0671997,1.3764-3.3084984v-17.6030006c-.0051003-1.1263008-.4159012-2.2131004-1.1570015-3.0612011-.7411003-.8480988-1.7628975-1.4008999-2.8782997-1.5570984Zm-11.7304993-15.767271c2.4372997,.0026903,4.7739983,.9721003,6.4973984,2.69557,1.7235031,1.7234001,2.6929016,4.0601006,2.6956024,6.4974003v6.5076008h-18.3860016v-6.5076008c.0027008-2.4372997,.9721012-4.7740002,2.6955013-6.4974003,1.7234993-1.7234697,4.0601997-2.6928797,6.4974995-2.69557Zm13.2282982,37.9899712c-.0003967,.5695-.2266998,1.1153984-.6293983,1.5181007-.4025993,.4025993-.9486008,.6289978-1.5180016,.6293983h-22.1617985c-.5693989-.0004005-1.1154003-.226799-1.5181007-.6293983-.4025993-.4027023-.6289988-.9486008-.6293993-1.5181007v-17.6044006c.0008001-.5692997,.2273006-1.1151009,.6299009-1.517601,.4025002-.4025993,.9482994-.6290989,1.5175991-.6299h22.1617985c.5693016,.0008011,1.1151009,.2273006,1.517601,.6299,.4025993,.4025002,.6291008,.9483013,.6297989,1.517601v17.6044006Z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,262 @@
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalMessaging
public protocol PaymentsBiometryLockPromptDelegate: AnyObject {
func didEnablePaymentsLock()
func didNotEnablePaymentsLock()
}
// MARK: -
public class PaymentsBiometryLockPromptViewController: OWSViewController {
private var hasBeenDoubleReminded: Bool = false
private let validBiometryType: ValidBiometryType
private weak var delegate: PaymentsBiometryLockPromptDelegate?
private let rootView = UIStackView()
public required init(biometryType: ValidBiometryType, delegate: PaymentsBiometryLockPromptDelegate?) {
self.validBiometryType = biometryType
self.delegate = delegate
super.init()
}
public override func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("SETTINGS_PAYMENTS_ENABLE_PAYMENTS_LOCK_PROMPT",
comment: "Title for the 'enable payments lock' view of the payments activation flow.")
OWSTableViewController2.removeBackButtonText(viewController: self)
rootView.axis = .vertical
rootView.alignment = .fill
view.addSubview(rootView)
rootView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
rootView.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
rootView.autoPinWidthToSuperviewMargins()
updateContents()
updateNavbar()
}
public override func themeDidChange() {
super.themeDidChange()
self.applyTheme()
}
public func applyTheme() {
updateContents()
}
private func updateNavbar() {
navigationItem.leftBarButtonItem = UIBarButtonItem(
image: UIImage(named: "x-24")?.withRenderingMode(.alwaysTemplate),
style: .plain,
target: self,
action: #selector(didTapClose),
accessibilityIdentifier: "close"
)
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateContents()
updateNavbar()
}
@objc
private func updateContents() {
AssertIsOnMainThread()
view.backgroundColor = OWSTableViewController2.tableBackgroundColor(isUsingPresentedStyle: true)
let heroImage = UIImageView(image: UIImage(named: "payments-lock"))
let titleLabel = UILabel()
titleLabel.text = NSLocalizedString("SETTINGS_PAYMENTS_VIEW_PAYMENTS_LOCK_PROMPT_TITLE",
comment: "Title for the content section of the 'payments lock prompt' view shown after payemts activation.")
titleLabel.font = UIFont.ows_dynamicTypeTitle2Clamped.ows_semibold
titleLabel.textColor = Theme.primaryTextColor
titleLabel.textAlignment = .center
let explanationLabel = UILabel()
explanationLabel.text = localizedExplanationLabelText()
explanationLabel.font = .ows_dynamicTypeBody2Clamped
explanationLabel.textColor = Theme.primaryTextColor
explanationLabel.textAlignment = .center
explanationLabel.numberOfLines = 0
let topStack = UIStackView(arrangedSubviews: [
heroImage,
UIView.spacer(withHeight: 20),
titleLabel,
UIView.spacer(withHeight: 10),
explanationLabel
])
topStack.axis = .vertical
topStack.alignment = .center
topStack.isLayoutMarginsRelativeArrangement = true
topStack.layoutMargins = UIEdgeInsets(hMargin: 20, vMargin: 0)
let enableButton = OWSFlatButton.button(title: enableButtonTitle(),
font: UIFont.ows_dynamicTypeBody.ows_semibold,
titleColor: .white,
backgroundColor: .ows_accentBlue,
target: self,
selector: #selector(didTapEnableButton))
enableButton.autoSetHeightUsingFont()
let notNowButton = OWSFlatButton.button(title: CommonStrings.notNowButton,
font: UIFont.ows_dynamicTypeBody.ows_semibold,
titleColor: .ows_accentBlue,
backgroundColor: .clear,
target: self,
selector: #selector(didTapNotNowButton))
notNowButton.autoSetHeightUsingFont()
let spacerFactory = SpacerFactory()
rootView.removeAllSubviews()
rootView.addArrangedSubviews([
spacerFactory.buildVSpacer(),
topStack,
spacerFactory.buildVSpacer(),
enableButton,
UIView.spacer(withHeight: 16),
notNowButton,
UIView.spacer(withHeight: 8)
])
spacerFactory.finalizeSpacers()
}
// MARK: - Events
@objc
func didTapClose() {
guard hasBeenDoubleReminded == false else {
dismiss(animated: true, completion: nil)
return
}
showDoubleReminder()
}
@objc
func didTapEnableButton() {
databaseStorage.write { transaction in
OWSPaymentsLock.shared.setIsPaymentsLockEnabled(true, transaction: transaction)
}
dismiss(animated: true, completion: nil)
}
@objc
func didTapNotNowButton() {
AssertIsOnMainThread()
guard hasBeenDoubleReminded == false else {
dismiss(animated: true, completion: nil)
return
}
showDoubleReminder()
}
func showDoubleReminder() {
AssertIsOnMainThread()
self.hasBeenDoubleReminded = true
let actionSheet = ActionSheetController(
title: doubleReminderActionSheetTitle(),
message: NSLocalizedString(
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_MESSAGE",
comment: "Description for the 'double reminder' action sheet in the 'payments lock prompt' view in the payment settings."))
actionSheet.addAction(
ActionSheetAction(
title: CommonStrings.skipButton,
accessibilityIdentifier: "OWSActionSheets.skip",
style: .destructive
) { [weak self] _ in
Logger.debug("User is explicity skipping the double reminder, so dismniss the 'payments lock prompt' view entirely.")
self?.dismiss(animated: true, completion: nil)
}
)
actionSheet.addAction(
ActionSheetAction(
title: CommonStrings.cancelButton,
accessibilityIdentifier: "OWSActionSheets.cancel",
style: .cancel
) { _ in
Logger.debug("User cancelled the payments lock dismissal, dismiss the action sheet so user can reconsider payments lock decision")
}
)
presentActionSheet(actionSheet)
}
private func localizedExplanationLabelText() -> String {
switch validBiometryType {
case .faceId:
return NSLocalizedString(
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_EXPLANATION_FACEID",
comment: "Explanation of 'payments lock' with Face ID in the 'payments lock prompt' view shown after payments activation.")
case .touchId:
return NSLocalizedString(
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_EXPLANATION_TOUCHID",
comment: "Explanation of 'payments lock' with Touch ID in the 'payments lock prompt' view shown after payments activation.")
case .passcode:
return NSLocalizedString(
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_EXPLANATION_PASSCODE",
comment: "Explanation of 'payments lock' with passcode in the 'payments lock prompt' view shown after payments activation.")
}
}
private func enableButtonTitle() -> String {
switch validBiometryType {
case .faceId:
return NSLocalizedString(
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_ENABLE_BUTTON_FACEID",
comment: "Enable Button title in Payments Lock Prompt view for Face ID.")
case .touchId:
return NSLocalizedString(
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_ENABLE_BUTTON_TOUCHID",
comment: "Enable Button title in Payments Lock Prompt view for Touch ID.")
case .passcode:
return NSLocalizedString(
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_ENABLE_BUTTON_PASSCODE",
comment: "Enable Button title in Payments Lock Prompt view for Passcode.")
}
}
private func doubleReminderActionSheetTitle() -> String {
switch validBiometryType {
case .faceId:
return NSLocalizedString(
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_TITLE_FACEID",
comment: "Double reminder action sheet title in Payments Lock Prompt view for Face ID.")
case .touchId:
return NSLocalizedString(
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_TITLE_TOUCHID",
comment: "Double reminder action sheet title in Payments Lock Prompt view for Touch ID.")
case .passcode:
return NSLocalizedString(
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_TITLE_PASSCODE",
comment: "Double reminder action sheet title in Payments Lock Prompt view for Passcode.")
}
}
}

View File

@ -4,6 +4,7 @@
//
import Foundation
import SignalUI
@objc
class PaymentsSendRecipientViewController: RecipientPickerContainerViewController {
@ -136,7 +137,15 @@ extension PaymentsSendRecipientViewController: RecipientPickerDelegate {
extension PaymentsSendRecipientViewController: SendPaymentViewDelegate {
func didSendPayment() {
dismiss(animated: true, completion: nil)
func didSendPayment(success: Bool) {
dismiss(animated: true) {
guard success else {
// only prompt users to enable payments lock when successful.
return
}
PaymentOnboarding.presentBiometricLockPromptIfNeeded {
Logger.debug("Payments Lock Request Complete")
}
}
}
}

View File

@ -947,6 +947,7 @@ public class PaymentsSettingsViewController: OWSTableViewController2 {
accessibilityIdentifier: "payments.settings.activate.agree",
style: .default) { [weak self] _ in
self?.enablePayments()
self?.promptBiometryPaymentsLock()
})
actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("SETTINGS_PAYMENTS_ACTIVATE_PAYMENTS_CONFIRM_VIEW_TERMS",
comment: "Label for the 'view payments terms' button in the 'activate payments confirmation' UI in the payment settings."),
@ -983,6 +984,19 @@ public class PaymentsSettingsViewController: OWSTableViewController2 {
}
}
private func promptBiometryPaymentsLock() {
AssertIsOnMainThread()
guard let validBiometryType = BiometryType.validBiometryType else {
owsFailDebug("Unknown biometry type, cannot enable payments lock")
return
}
let view = PaymentsBiometryLockPromptViewController(biometryType: validBiometryType, delegate: nil)
let navigationVC = OWSNavigationController(rootViewController: view)
present(navigationVC, animated: true)
}
private func showPaymentsActivatedToast() {
AssertIsOnMainThread()
let toastText = NSLocalizedString("SETTINGS_PAYMENTS_OPT_IN_ACTIVATED_TOAST",

View File

@ -7,6 +7,7 @@ import Foundation
import Lottie
import MobileCoin
import SignalMessaging
import SignalUI
@objc
public class PaymentsTransferOutViewController: OWSTableViewController2 {
@ -206,8 +207,16 @@ extension PaymentsTransferOutViewController: UITextFieldDelegate {
// MARK: -
extension PaymentsTransferOutViewController: SendPaymentViewDelegate {
public func didSendPayment() {
dismiss(animated: true, completion: nil)
public func didSendPayment(success: Bool) {
dismiss(animated: true) {
guard success else {
// only prompt users to enable payments lock when successful.
return
}
PaymentOnboarding.presentBiometricLockPromptIfNeeded {
Logger.debug("Payments Lock Request Complete")
}
}
}
}

View File

@ -149,8 +149,26 @@ public class PaymentsViewPassphraseSplashViewController: OWSViewController {
return
}
let view = PaymentsViewPassphraseGridViewController(passphrase: passphrase,
viewPassphraseDelegate: viewPassphraseDelegate)
navigationController?.pushViewController(view, animated: true)
if OWSPaymentsLock.shared.isPaymentsLockEnabled() {
OWSPaymentsLock.shared.tryToUnlock { [weak self] outcome in
guard let self = self else { return }
guard outcome == OWSPaymentsLock.LocalAuthOutcome.success else {
PaymentActionSheets.showBiometryAuthFailedActionSheet { _ in
self.dismiss(animated: false, completion: nil)
}
return
}
let view = PaymentsViewPassphraseGridViewController(
passphrase: self.passphrase,
viewPassphraseDelegate: viewPassphraseDelegate)
self.navigationController?.pushViewController(view, animated: true)
}
} else {
let view = PaymentsViewPassphraseGridViewController(
passphrase: passphrase,
viewPassphraseDelegate: viewPassphraseDelegate)
navigationController?.pushViewController(view, animated: true)
}
}
}

View File

@ -185,6 +185,32 @@ class PrivacySettingsViewController: OWSTableViewController2 {
}
contents.addSection(appSecuritySection)
// Payments
let paymentsSection = OWSTableSection()
paymentsSection.headerTitle = NSLocalizedString("SETTINGS_PAYMENTS_SECURITY_TITLE", comment: "Title for the payments section in the apps privacy settings tableview")
switch BiometryType.biometryType {
case .unknown:
paymentsSection.footerTitle = NSLocalizedString("SETTINGS_PAYMENTS_SECURITY_DETAIL", comment: "Caption for footer label beneath the payments lock privacy toggle for a biometry type that is unknown.")
case .passcode:
paymentsSection.footerTitle = NSLocalizedString("SETTINGS_PAYMENTS_SECURITY_DETAIL_PASSCODE", comment: "Caption for footer label beneath the payments lock privacy toggle for a biometry type that is a passcode.")
case .faceId:
paymentsSection.footerTitle = NSLocalizedString("SETTINGS_PAYMENTS_SECURITY_DETAIL_FACEID", comment: "Caption for footer label beneath the payments lock privacy toggle for faceid biometry.")
case .touchId:
paymentsSection.footerTitle = NSLocalizedString("SETTINGS_PAYMENTS_SECURITY_DETAIL_TOUCHID", comment: "Caption for footer label beneath the payments lock privacy toggle for touchid biometry")
}
paymentsSection.add(.switch(
withText: NSLocalizedString(
"SETTINGS_PAYMENTS_LOCK_SWITCH_LABEL",
comment: "Label for UISwitch based payments-lock setting that when enabled requires biometric-authentication (or passcode) to transfer funds or view the recovery phrase."
),
isOn: { OWSPaymentsLock.shared.isPaymentsLockEnabled() },
target: self,
selector: #selector(didTogglePaymentsLockSwitch)
))
contents.addSection(paymentsSection)
if !CallUIAdapter.isCallkitDisabledForLocale {
let callsSection = OWSTableSection()
callsSection.headerTitle = NSLocalizedString(
@ -248,6 +274,30 @@ class PrivacySettingsViewController: OWSTableViewController2 {
updateTableContents()
}
@objc
func didTogglePaymentsLockSwitch(_ sender: UISwitch) {
// Require unlock to disable payments lock
if OWSPaymentsLock.shared.isPaymentsLockEnabled() {
OWSPaymentsLock.shared.tryToUnlock { [weak self] outcome in
guard let self = self else { return }
guard case .success = outcome else {
self.updateTableContents()
PaymentActionSheets.showBiometryAuthFailedActionSheet()
return
}
self.databaseStorage.write { transaction in
OWSPaymentsLock.shared.setIsPaymentsLockEnabled(false, transaction: transaction)
}
self.updateTableContents()
}
} else {
databaseStorage.write { transaction in
OWSPaymentsLock.shared.setIsPaymentsLockEnabled(true, transaction: transaction)
}
self.updateTableContents()
}
}
private func showScreenLockTimeoutPicker() {
let actionSheet = ActionSheetController(title: NSLocalizedString(
"SETTINGS_SCREEN_LOCK_ACTIVITY_TIMEOUT",

View File

@ -447,9 +447,23 @@ extension ConversationViewController: LongTextViewDelegate {
// MARK: -
extension ConversationViewController: SendPaymentViewDelegate {
public func didSendPayment() {
let paymentSettingsView = PaymentsSettingsViewController(mode: .standalone)
let navigationController = OWSNavigationController(rootViewController: paymentSettingsView)
presentFormSheet(navigationController, animated: true)
public func didSendPayment(success: Bool) {
func paymentSettingsNavigationController() -> OWSNavigationController {
let paymentSettingsView = PaymentsSettingsViewController(mode: .standalone)
return OWSNavigationController(rootViewController: paymentSettingsView)
}
// only prompt users to enable payments lock when successful.
guard success else {
// TODO - Remove when in-chat payment bubble implemented.
self.presentFormSheet(paymentSettingsNavigationController(), animated: true)
return
}
PaymentOnboarding.presentBiometricLockPromptIfNeeded { [weak self] in
// TODO - Remove when in-chat payment bubble implemented.
self?.presentFormSheet(paymentSettingsNavigationController(), animated: true)
}
}
}

View File

@ -10,7 +10,7 @@ import UIKit
@objc
public protocol SendPaymentCompletionDelegate {
func didSendPayment()
func didSendPayment(success: Bool)
}
// MARK: -
@ -516,6 +516,15 @@ public class SendPaymentCompletionActionSheet: ActionSheetController {
return NSLocalizedString("PAYMENTS_NEW_PAYMENT_ERROR_UNKNOWN",
comment: "Indicates that an unknown error occurred while sending a payment or payment request.")
}
case let paymentsError as PaymentsUIError:
switch paymentsError {
case .paymentsLockFailed:
return NSLocalizedString("PAYMENTS_NEW_PAYMENT_ERROR_PAYMENTS_LOCK_AUTH_FAILURE",
comment: "Indicates that a payment failed because the payments lock failed to authenticate.")
case .paymentsLockCancelled:
return NSLocalizedString("PAYMENTS_NEW_PAYMENT_ERROR_PAYMENTS_LOCK_AUTH_CANCELLED",
comment: "Indicates that a payment failed because the payments lock attempt was cancelled.")
}
default:
return NSLocalizedString("PAYMENTS_NEW_PAYMENT_ERROR_UNKNOWN",
comment: "Indicates that an unknown error occurred while sending a payment or payment request.")
@ -583,7 +592,20 @@ public class SendPaymentCompletionActionSheet: ActionSheetController {
ModalActivityIndicatorViewController.presentAsInvisible(fromViewController: self) { [weak self] modalActivityIndicator in
guard let self = self else { return }
firstly(on: .global()) { () -> Promise<PreparedPayment> in
OWSPaymentsLock.shared.tryToUnlockPromise().then(on: .main) { (authOutcome: OWSPaymentsLock.LocalAuthOutcome) -> Promise<PreparedPayment> in
switch authOutcome {
case .failure(let error):
throw PaymentsUIError.paymentsLockFailed(reason: "local authentication failed with error: \(error)")
case .unexpectedFailure(let error):
throw PaymentsUIError.paymentsLockFailed(reason: "local authentication failed with unexpected error: \(error)")
case .success:
Logger.verbose("payments lock local authentication succeeded.")
case .cancel:
throw PaymentsUIError.paymentsLockCancelled(reason: "local authentication cancelled")
case .disabled:
Logger.verbose("payments lock not enabled.")
}
guard let promise = self.preparedPaymentPromise.get() else {
throw OWSAssertionError("Missing preparedPaymentPromise.")
}
@ -633,9 +655,8 @@ public class SendPaymentCompletionActionSheet: ActionSheetController {
AssertIsOnMainThread()
owsFailDebugUnlessMCNetworkFailure(error)
modalActivityIndicator.dismiss {}
self.didFailPayment(paymentInfo: paymentInfo, error: error)
modalActivityIndicator.dismiss()
}
}
}
@ -649,7 +670,7 @@ public class SendPaymentCompletionActionSheet: ActionSheetController {
DispatchQueue.main.asyncAfter(deadline: .now() + Self.autoDismissDelay) { [weak self] in
guard let self = self else { return }
self.dismiss(animated: true) {
delegate?.didSendPayment()
delegate?.didSendPayment(success: true)
}
}
}
@ -661,7 +682,9 @@ public class SendPaymentCompletionActionSheet: ActionSheetController {
DispatchQueue.main.asyncAfter(deadline: .now() + Self.autoDismissDelay) { [weak self] in
guard let self = self else { return }
self.dismiss(animated: true) {
delegate?.didSendPayment()
PaymentActionSheets.showBiometryAuthFailedActionSheet { _ in
delegate?.didSendPayment(success: false)
}
}
}
}

View File

@ -9,7 +9,7 @@ import UIKit
@objc
public protocol SendPaymentViewDelegate {
func didSendPayment()
func didSendPayment(success: Bool)
}
// MARK: -
@ -1178,10 +1178,10 @@ extension SendPaymentViewController: SendPaymentHelperDelegate {
// MARK: -
extension SendPaymentViewController: SendPaymentCompletionDelegate {
public func didSendPayment() {
public func didSendPayment(success: Bool) {
let delegate = self.delegate
self.dismiss(animated: true) {
delegate?.didSendPayment()
delegate?.didSendPayment(success: success)
}
}
}

View File

@ -4333,6 +4333,60 @@
/* Status indicator for outgoing payments which failed to verify. */
"PAYMENTS_FAILURE_OUTGOING_VALIDATION_FAILED" = "Invalid";
/* Indicates that an unknown error occurred while using Touch ID/Face ID/Phone Passcode. */
"PAYMENTS_LOCK_AUTHENTICATION_ENABLE_UNKNOWN_ERROR" = "Authentication could not be accessed.";
/* Indicates that Touch ID/Face ID/Phone Passcode authentication failed. */
"PAYMENTS_LOCK_ERROR_LOCAL_AUTHENTICATION_FAILED" = "Authentication failed.";
/* Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures. */
"PAYMENTS_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT" = "Too many failed authentication attempts. Please try again later.";
/* Indicates that Touch ID/Face ID/Phone Passcode are not available on this device. */
"PAYMENTS_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE" = "You must enable a passcode in your iOS Settings in order to use Payments Lock.";
/* Indicates that Touch ID/Face ID/Phone Passcode is not configured on this device. */
"PAYMENTS_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED" = "You must enable a passcode in your iOS Settings in order to use Payments Lock.";
/* Indicates that Touch ID/Face ID/Phone Passcode passcode is not set. */
"PAYMENTS_LOCK_ERROR_LOCAL_AUTHENTICATION_PASSCODE_NOT_SET" = "You must enable a passcode in your iOS Settings in order to use Payments Lock.";
/* First time payments suggest payments lock message */
"PAYMENTS_LOCK_FIRST_TIME_ACTION_SHEET_MESSAGE" = "Add an additional layer of security and require your passcode or Touch ID to send funds";
/* First time payments suggest payments lock message */
"PAYMENTS_LOCK_FIRST_TIME_ACTION_SHEET_MESSAGE_FACEID" = "Add an additional layer of security and require Face ID to send funds.";
/* First time payments suggest payments lock message */
"PAYMENTS_LOCK_FIRST_TIME_ACTION_SHEET_MESSAGE_PASSCODE" = "Add an additional layer of security and require passcode to send funds.";
/* First time payments suggest payments lock message */
"PAYMENTS_LOCK_FIRST_TIME_ACTION_SHEET_MESSAGE_TOUCHID" = "Add an additional layer of security and require Touch ID to send funds.";
/* First time payments suggest payments lock title */
"PAYMENTS_LOCK_FIRST_TIME_ACTION_SHEET_TITLE" = "Turn on Payment Lock for Future Sends?";
/* Affirmative action title to enable payments lock */
"PAYMENTS_LOCK_FIRST_TIME_AFFIRMATIVE_ACTION" = "Require Your Passcode or Touch ID to Send";
/* Affirmative action title to enable payments lock */
"PAYMENTS_LOCK_FIRST_TIME_AFFIRMATIVE_ACTION_FACEID" = "Require Face ID to Send";
/* Affirmative action title to enable payments lock */
"PAYMENTS_LOCK_FIRST_TIME_AFFIRMATIVE_ACTION_PASSCODE" = "Require Your Phone's Passcode to Send";
/* Affirmative action title to enable payments lock */
"PAYMENTS_LOCK_FIRST_TIME_AFFIRMATIVE_ACTION_TOUCHID" = "Require Touch ID to Send";
/* Message for action sheet shown when unlocking with biometrics like Face ID or TouchID fails because it is disabled at a system level. */
"PAYMENTS_LOCK_LOCAL_BIOMETRY_AUTH_DISABLED_MESSAGE" = "Authentication did not succeed. Ensure that biometrics is enabled on your device and a passcode is set.";
/* Title for action sheet shown when unlocking with biometrics like Face ID or TouchID fails because it is disabled at a system level. */
"PAYMENTS_LOCK_LOCAL_BIOMETRY_AUTH_DISABLED_TITLE" = "Biometric Authentication Failed";
/* Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to unlock 'payments lock'. */
"PAYMENTS_LOCK_REASON_UNLOCK_PAYMENTS_LOCK" = "Authenticate to confirm payment.";
/* Label for the 'add memo' ui in the 'send payment' UI. */
"PAYMENTS_NEW_PAYMENT_ADD_MEMO" = "Add Note";
@ -4366,6 +4420,12 @@
/* Indicates that an outgoing payment could not be verified in a timely way. */
"PAYMENTS_NEW_PAYMENT_ERROR_OUTGOING_VERIFICATION_TAKING_TOO_LONG" = "Payment not yet verified";
/* Indicates that a payment failed because the payments lock attempt was cancelled. */
"PAYMENTS_NEW_PAYMENT_ERROR_PAYMENTS_LOCK_AUTH_CANCELLED" = "Payments lock authentication cancelled.";
/* Indicates that a payment failed because the payments lock failed to authenticate. */
"PAYMENTS_NEW_PAYMENT_ERROR_PAYMENTS_LOCK_AUTH_FAILURE" = "Payments lock authentication failure.";
/* Indicates that an unknown error occurred while sending a payment or payment request. */
"PAYMENTS_NEW_PAYMENT_ERROR_UNKNOWN" = "Couldn't complete payment. Check your connection and try again.";
@ -5764,6 +5824,9 @@
/* Label for the 'enable payments' button in the 'payments not enabled' alert. */
"SETTINGS_PAYMENTS_ENABLE_ACTION" = "Enable Payments";
/* Title for the 'enable payments lock' view of the payments activation flow. */
"SETTINGS_PAYMENTS_ENABLE_PAYMENTS_LOCK_PROMPT" = "Payments Lock";
/* Description for the 'About MobileCoin' help card in the payments settings. */
"SETTINGS_PAYMENTS_HELP_CARD_ABOUT_MOBILECOIN_DESCRIPTION" = "MobileCoin is a new privacy focused digital currency.";
@ -5803,6 +5866,9 @@
/* Indicator that the payments wallet address is invalid. */
"SETTINGS_PAYMENTS_INVALID_WALLET_ADDRESS" = "Invalid Wallet Address";
/* Label for UISwitch based payments-lock setting that when enabled requires biometric-authentication (or passcode) to transfer funds or view the recovery phrase. */
"SETTINGS_PAYMENTS_LOCK_SWITCH_LABEL" = "Payments Lock";
/* Message indicating that there is no payment activity to display in the payment settings. */
"SETTINGS_PAYMENTS_NO_ACTIVITY_INDICATOR" = "No recent activity yet.";
@ -5887,6 +5953,36 @@
/* Message indicating that payments have been disabled in the app settings. */
"SETTINGS_PAYMENTS_PAYMENTS_DISABLED_TOAST" = "Payments deactivated.";
/* Description for the 'double reminder' action sheet in the 'payments lock prompt' view in the payment settings. */
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_MESSAGE" = "Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase.";
/* Double reminder action sheet title in Payments Lock Prompt view for Face ID. */
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_TITLE_FACEID" = "Skip Enabling Face ID?";
/* Double reminder action sheet title in Payments Lock Prompt view for Passcode. */
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_TITLE_PASSCODE" = "Skip Enabling Passcode?";
/* Double reminder action sheet title in Payments Lock Prompt view for Touch ID. */
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_TITLE_TOUCHID" = "Skip Enabling Touch ID?";
/* Enable Button title in Payments Lock Prompt view for Face ID. */
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_ENABLE_BUTTON_FACEID" = "Use Face ID";
/* Enable Button title in Payments Lock Prompt view for Passcode. */
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_ENABLE_BUTTON_PASSCODE" = "Use Passcode";
/* Enable Button title in Payments Lock Prompt view for Touch ID. */
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_ENABLE_BUTTON_TOUCHID" = "Use Touch ID";
/* Explanation of 'payments lock' with Face ID in the 'payments lock prompt' view shown after payments activation. */
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_EXPLANATION_FACEID" = "Help prevent a person with your phone from accessing your funds by enabling Face ID. You can disable this option in Settings.";
/* Explanation of 'payments lock' with passcode in the 'payments lock prompt' view shown after payments activation. */
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_EXPLANATION_PASSCODE" = "Help prevent a person with your phone from accessing your funds by using your passcode. You can disable this option in Settings.";
/* Explanation of 'payments lock' with Touch ID in the 'payments lock prompt' view shown after payments activation. */
"SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_EXPLANATION_TOUCHID" = "Help prevent a person with your phone from accessing your funds by enabling Touch ID. You can disable this option in Settings.";
/* Button for payments outdated sheet. */
"SETTINGS_PAYMENTS_PAYMENTS_OUTDATED_BUTTON" = "Update Signal";
@ -5989,6 +6085,21 @@
/* Label for 'scan payment address QR code' view in the payment settings. */
"SETTINGS_PAYMENTS_SCAN_QR_TITLE" = "Scan QR Code";
/* Caption for footer label beneath the payments lock privacy toggle for a biometry type that is unknown. */
"SETTINGS_PAYMENTS_SECURITY_DETAIL" = "Require your passcode or Touch ID to transfer funds.";
/* Caption for footer label beneath the payments lock privacy toggle for faceid biometry. */
"SETTINGS_PAYMENTS_SECURITY_DETAIL_FACEID" = "Require Face ID to transfer funds.";
/* Caption for footer label beneath the payments lock privacy toggle for a biometry type that is a passcode. */
"SETTINGS_PAYMENTS_SECURITY_DETAIL_PASSCODE" = "Require your phone's passcode to transfer funds.";
/* Caption for footer label beneath the payments lock privacy toggle for touchid biometry */
"SETTINGS_PAYMENTS_SECURITY_DETAIL_TOUCHID" = "Require Touch ID to transfer funds.";
/* Title for the payments section in the apps privacy settings tableview */
"SETTINGS_PAYMENTS_SECURITY_TITLE" = "Payments";
/* Label for 'send payment' button in the payment settings. */
"SETTINGS_PAYMENTS_SEND_PAYMENT" = "Send Payment";
@ -6070,6 +6181,9 @@
/* Footer text for the 'review payments passphrase words' step in the 'view payments passphrase' settings. */
"SETTINGS_PAYMENTS_VIEW_PASSPHRASE_WORDS_FOOTER" = "Do not screenshot or send by email.";
/* Title for the content section of the 'payments lock prompt' view shown after payemts activation. */
"SETTINGS_PAYMENTS_VIEW_PAYMENTS_LOCK_PROMPT_TITLE" = "Another Layer of Protection";
/* Label for 'view payments recovery passphrase' button in the app settings. */
"SETTINGS_PAYMENTS_VIEW_RECOVERY_PASSPHRASE" = "Recovery Phrase";

View File

@ -0,0 +1,59 @@
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LocalAuthentication
public enum BiometryType {
case unknown, passcode, faceId, touchId
}
extension BiometryType {
public static func localAuthenticationContext() -> LAContext {
let context = LAContext()
// Never recycle biometric auth.
context.touchIDAuthenticationAllowableReuseDuration = TimeInterval(0)
assert(!context.interactionNotAllowed)
return context
}
public static var biometryType: BiometryType {
let context = localAuthenticationContext()
var authError: NSError?
let canEvaluatePolicy = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &authError)
switch context.biometryType {
case .none:
return .passcode
case .faceID:
return .faceId
case .touchID:
return .touchId
@unknown default:
return .unknown
}
}
public static var validBiometryType: ValidBiometryType? {
switch biometryType {
case .unknown:
return nil
case .passcode:
return .passcode
case .faceId:
return .faceId
case .touchId:
return .touchId
}
}
}
public enum ValidBiometryType {
case passcode, faceId, touchId
}

View File

@ -0,0 +1,292 @@
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LocalAuthentication
public class OWSPaymentsLock: Dependencies {
public enum LocalAuthOutcome: Equatable {
case success
case cancel
case disabled
case failure(error: String)
case unexpectedFailure(error: String)
}
// MARK: - Singleton class
public static let shared = OWSPaymentsLock()
init() {
SwiftSingletons.register(self)
}
// MARK: - KV Store
private let keyValueStore = SDSKeyValueStore(collection: "OWSPaymentsLock")
// MARK: - Properties
public func isPaymentsLockEnabled() -> Bool {
AssertIsOnMainThread()
guard AppReadiness.isAppReady else {
owsFailDebug("accessed payments lock state before storage is ready.")
// `true` is a more secure default
return true
}
return databaseStorage.read { transaction in
return self.keyValueStore.getBool(.isPaymentsLockEnabledKey,
defaultValue: false,
transaction: transaction)
}
}
public func setIsPaymentsLockEnabledAndSnooze(_ value: Bool) {
databaseStorage.write { transaction in
setIsPaymentsLockEnabled(value, transaction: transaction)
snoozeSuggestion(transaction: transaction)
}
}
public func setIsPaymentsLockEnabled(_ value: Bool, transaction: SDSAnyWriteTransaction) {
AssertIsOnMainThread()
assert(AppReadiness.isAppReady)
self.keyValueStore.setBool(value,
key: .isPaymentsLockEnabledKey,
transaction: transaction)
}
public func isTimeToShowSuggestion() -> Bool {
AssertIsOnMainThread()
if !AppReadiness.isAppReady {
owsFailDebug("accessed payments lock state before storage is ready.")
return false
}
let defaultDate = Date.distantPast
let date = databaseStorage.read { transaction in
return self.keyValueStore.getDate(.timeToShowSuggestionKey,
transaction: transaction) ?? defaultDate
}
return Date() > date
}
public func snoozeSuggestion(transaction: SDSAnyWriteTransaction) {
AssertIsOnMainThread()
assert(AppReadiness.isAppReady)
let currentDate = Date()
let numberOfSnoozeDays = 30.0
let nextTimeToShowSuggestion = currentDate.addingTimeInterval(
Double(numberOfSnoozeDays * kDayInterval)
)
self.keyValueStore.setDate(nextTimeToShowSuggestion,
key: .timeToShowSuggestionKey,
transaction: transaction)
}
// MARK: - Biometry Types
// This method should only be called:
//
// * On the main thread.
//
// completionParam will be performed:
//
// * Asynchronously.
// * On the main thread.
public func tryToUnlock(
completion completionParam: @escaping ((LocalAuthOutcome) -> Void)
) {
AssertIsOnMainThread()
// Ensure completion is always called on the main thread.
let completion = { (outcome: LocalAuthOutcome) in
DispatchQueue.main.async {
completionParam(outcome)
}
}
guard self.isPaymentsLockEnabled() else {
completion(.disabled)
return
}
let context = BiometryType.localAuthenticationContext()
var authError: NSError?
let canEvaluatePolicy = context.canEvaluatePolicy(
.deviceOwnerAuthentication,
error: &authError)
guard canEvaluatePolicy && authError == nil else {
Logger.error("could not determine if local authentication is supported: " +
"\(String(describing: authError))")
let outcome = outcomeForLAError(errorParam: authError)
switch outcome {
case .success:
owsFailDebug("local authentication unexpected success")
completion(.failure(error: .localizedDefaultErrorDescription))
case .cancel, .failure, .unexpectedFailure, .disabled:
completion(outcome)
}
return
}
context.evaluatePolicy(
.deviceOwnerAuthentication,
localizedReason: .localizedAuthReason
) { success, evaluateError in
guard success else {
let outcome = self.outcomeForLAError(errorParam: evaluateError)
switch outcome {
case .success:
owsFailDebug("local authentication unexpected success")
completion(.failure(error: .localizedDefaultErrorDescription))
case .cancel, .failure, .unexpectedFailure, .disabled:
completion(outcome)
}
return
}
Logger.info("local authentication succeeded.")
completion(.success)
}
}
public func tryToUnlockPromise() -> Promise<OWSPaymentsLock.LocalAuthOutcome> {
Promise<OWSPaymentsLock.LocalAuthOutcome>(on: .main) { future in
OWSPaymentsLock.shared.tryToUnlock { outcome in
future.resolve(outcome)
}
}
}
// MARK: - Outcome
private func outcomeForLAError(errorParam: Error?) -> LocalAuthOutcome {
guard let error = errorParam,
let laError = error as? LAError
else {
return .failure(error: .localizedDefaultErrorDescription)
}
return LocalAuthOutcome.outcomeFromLAError(
laError,
defaultErrorDescription: .localizedDefaultErrorDescription)
}
}
// MARK: - File-Specific Constants & Computed Values
fileprivate extension String {
static let isPaymentsLockEnabledKey = "isPaymentsLockEnabled"
static let timeToShowSuggestionKey = "timeToShowSuggestion"
// Localized String Constants
static var localizedDefaultErrorDescription: String {
OWSLocalizedString(
"PAYMENTS_LOCK_AUTHENTICATION_ENABLE_UNKNOWN_ERROR",
comment: "Indicates that an unknown error occurred while using Touch ID/Face ID/Phone Passcode.")
}
static var localizedAuthReason: String {
OWSLocalizedString(
"PAYMENTS_LOCK_REASON_UNLOCK_PAYMENTS_LOCK",
comment: "Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to unlock 'payments lock'.")
}
}
fileprivate extension OWSPaymentsLock.LocalAuthOutcome {
static func outcomeFromLAError(
_ laError: LAError,
defaultErrorDescription: String
) -> OWSPaymentsLock.LocalAuthOutcome {
switch laError.code {
case .biometryNotAvailable:
Logger.error("local authentication error: biometryNotAvailable.")
return .failure(error: LAError.notAvailableLocalized)
case .biometryNotEnrolled:
Logger.error("local authentication error: biometryNotEnrolled.")
return .failure(error: LAError.notEnrolledLocalized)
case .biometryLockout:
Logger.error("local authentication error: biometryLockout.")
return .failure(error: LAError.lockoutLocalized)
case .authenticationFailed:
Logger.error("local authentication error: authenticationFailed.")
return .failure(error: LAError.authenticationFailedLocalized)
case .passcodeNotSet:
Logger.error("local authentication error: passcodeNotSet.")
return .failure(error: LAError.passcodeNotSetLocalized)
case .touchIDNotAvailable:
Logger.error("local authentication error: touchIDNotAvailable.")
return .failure(error: LAError.notAvailableLocalized)
case .touchIDNotEnrolled:
Logger.error("local authentication error: touchIDNotEnrolled.")
return .failure(error: LAError.notEnrolledLocalized)
case .touchIDLockout:
Logger.error("local authentication error: touchIDLockout.")
return .failure(error: LAError.lockoutLocalized)
case .userCancel, .userFallback, .systemCancel, .appCancel:
Logger.info("local authentication cancelled.")
return .cancel
case .invalidContext:
owsFailDebug("context not valid.")
return .unexpectedFailure(error: defaultErrorDescription)
case .notInteractive:
owsFailDebug("context not interactive.")
return .unexpectedFailure(error: defaultErrorDescription)
@unknown default:
owsFailDebug("Unexpected enum value.")
return .unexpectedFailure(error: defaultErrorDescription)
}
}
}
fileprivate extension LAError {
// Localized LAError Descriptions
static var authenticationFailedLocalized: String {
OWSLocalizedString(
"PAYMENTS_LOCK_ERROR_LOCAL_AUTHENTICATION_FAILED",
comment: "Indicates that Touch ID/Face ID/Phone Passcode authentication failed.")
}
static var passcodeNotSetLocalized: String {
OWSLocalizedString(
"PAYMENTS_LOCK_ERROR_LOCAL_AUTHENTICATION_PASSCODE_NOT_SET",
comment: "Indicates that Touch ID/Face ID/Phone Passcode passcode is not set.")
}
static var notAvailableLocalized: String {
OWSLocalizedString(
"PAYMENTS_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE",
comment: "Indicates that Touch ID/Face ID/Phone Passcode are not available on this device.")
}
static var notEnrolledLocalized: String {
OWSLocalizedString(
"PAYMENTS_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED",
comment: "Indicates that Touch ID/Face ID/Phone Passcode is not configured on this device.")
}
static var lockoutLocalized: String {
OWSLocalizedString(
"PAYMENTS_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT",
comment: "Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures.")
}
}

View File

@ -852,9 +852,11 @@ static NSString *_Nullable queryParamForIdentity(OWSIdentity identity)
+ (TSRequest *)subscriptionCreateStripePaymentMethodRequest:(NSString *)base64SubscriberID
{
TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:[NSString stringWithFormat:@"/v1/subscription/%@/create_payment_method", base64SubscriberID]]
method:@"POST"
parameters:@{}];
TSRequest *request = [TSRequest
requestWithUrl:[NSURL URLWithString:[NSString stringWithFormat:@"/v1/subscription/%@/create_payment_method",
base64SubscriberID]]
method:@"POST"
parameters:@{}];
request.shouldHaveAuthorizationHeaders = NO;
request.shouldRedactUrlInLogs = YES;
return request;

View File

@ -5,6 +5,11 @@
import Foundation
public enum PaymentsUIError: Error {
case paymentsLockFailed(reason: String)
case paymentsLockCancelled(reason: String)
}
public enum PaymentsError: Error {
case notEnabled
case userNotRegisteredOrAppNotReady

View File

@ -0,0 +1,25 @@
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalMessaging
public class PaymentActionSheets {
public static func showBiometryAuthFailedActionSheet(_ handler: ActionSheetAction.Handler? = nil) {
let title = NSLocalizedString(
"PAYMENTS_LOCK_LOCAL_BIOMETRY_AUTH_DISABLED_TITLE",
comment: "Title for action sheet shown when unlocking with biometrics like Face ID or TouchID fails because it is disabled at a system level.")
let message = NSLocalizedString(
"PAYMENTS_LOCK_LOCAL_BIOMETRY_AUTH_DISABLED_MESSAGE",
comment: "Message for action sheet shown when unlocking with biometrics like Face ID or TouchID fails because it is disabled at a system level.")
OWSActionSheets.showActionSheet(
title: title,
message: message,
buttonTitle: CommonStrings.okButton,
buttonAction: handler
)
}
}

View File

@ -0,0 +1,83 @@
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalMessaging
public class PaymentOnboarding {
private class func ftPaymentsLockActionSheetMessage() -> String {
switch BiometryType.biometryType {
case .unknown:
return NSLocalizedString(
"PAYMENTS_LOCK_FIRST_TIME_ACTION_SHEET_MESSAGE",
comment: "First time payments suggest payments lock message")
case .passcode:
return NSLocalizedString(
"PAYMENTS_LOCK_FIRST_TIME_ACTION_SHEET_MESSAGE_PASSCODE",
comment: "First time payments suggest payments lock message")
case .faceId:
return NSLocalizedString(
"PAYMENTS_LOCK_FIRST_TIME_ACTION_SHEET_MESSAGE_FACEID",
comment: "First time payments suggest payments lock message")
case .touchId:
return NSLocalizedString(
"PAYMENTS_LOCK_FIRST_TIME_ACTION_SHEET_MESSAGE_TOUCHID",
comment: "First time payments suggest payments lock message")
}
}
private class func ftPaymentsLockAffirmativeActionTitle() -> String {
switch BiometryType.biometryType {
case .unknown:
return NSLocalizedString(
"PAYMENTS_LOCK_FIRST_TIME_AFFIRMATIVE_ACTION",
comment: "Affirmative action title to enable payments lock")
case .passcode:
return NSLocalizedString(
"PAYMENTS_LOCK_FIRST_TIME_AFFIRMATIVE_ACTION_PASSCODE",
comment: "Affirmative action title to enable payments lock")
case .faceId:
return NSLocalizedString(
"PAYMENTS_LOCK_FIRST_TIME_AFFIRMATIVE_ACTION_FACEID",
comment: "Affirmative action title to enable payments lock")
case .touchId:
return NSLocalizedString(
"PAYMENTS_LOCK_FIRST_TIME_AFFIRMATIVE_ACTION_TOUCHID",
comment: "Affirmative action title to enable payments lock")
}
}
public class func presentBiometricLockPromptIfNeeded(completion: @escaping () -> Void) {
guard OWSPaymentsLock.shared.isTimeToShowSuggestion()
&& OWSPaymentsLock.shared.isPaymentsLockEnabled() == false
else {
completion()
return
}
let actionSheet = ActionSheetController(title: NSLocalizedString("PAYMENTS_LOCK_FIRST_TIME_ACTION_SHEET_TITLE",
comment: "First time payments suggest payments lock title"),
message: ftPaymentsLockActionSheetMessage())
actionSheet.addAction(ActionSheetAction(title: ftPaymentsLockAffirmativeActionTitle(),
accessibilityIdentifier: "payments.lock.first_time.affirmative_action",
style: .default) { _ in
OWSPaymentsLock.shared.setIsPaymentsLockEnabledAndSnooze(true)
completion()
})
actionSheet.addAction(ActionSheetAction(
title: CommonStrings.notNowButton,
accessibilityIdentifier: "OWSActionSheets.notNow",
style: .cancel
) { _ in
Logger.debug("Not Now")
OWSPaymentsLock.shared.setIsPaymentsLockEnabledAndSnooze(false)
completion()
})
OWSActionSheets.showActionSheet(actionSheet)
}
}