[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:
parent
2f384bf7a9
commit
728862da6f
@ -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 */,
|
||||
|
||||
12
Signal/Images.xcassets/payments-lock.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/payments-lock.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "payments-lock.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Signal/Images.xcassets/payments-lock.imageset/payments-lock.svg
vendored
Normal file
1
Signal/Images.xcassets/payments-lock.imageset/payments-lock.svg
vendored
Normal 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 |
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 app’s 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",
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 app’s 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";
|
||||
|
||||
|
||||
59
SignalMessaging/utils/BiometryType.swift
Normal file
59
SignalMessaging/utils/BiometryType.swift
Normal 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
|
||||
}
|
||||
292
SignalMessaging/utils/OWSPaymentsLock.swift
Normal file
292
SignalMessaging/utils/OWSPaymentsLock.swift
Normal 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.")
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
25
SignalUI/Views/PaymentActionSheets.swift
Normal file
25
SignalUI/Views/PaymentActionSheets.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
83
SignalUI/Views/PaymentOnboarding.swift
Normal file
83
SignalUI/Views/PaymentOnboarding.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user