Signal-iOS/Signal/test/ViewControllers/BadgeExpirationSheetStateTest.swift
Max Radermacher def8129ac8
Handle expired gift badges
- Show an error when trying to redeem them
- Show an error if your last remaining gift badge expires
2022-07-27 13:15:58 -07:00

273 lines
15 KiB
Swift

//
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
//
import XCTest
import Foundation
import Signal
class BadgeExpirationSheetStateTest: XCTestCase {
typealias State = BadgeExpirationSheetState
private func getBoostBadge(populateAssets: Bool = true) -> ProfileBadge {
let result = try! ProfileBadge(jsonDictionary: [
"id": "BOOST",
"category": "donor",
"name": "A Boost",
"description": "A boost badge!",
"sprites6": ["ldpi.png", "mdpi.png", "hdpi.png", "xhdpi.png", "xxhdpi.png", "xxxhdpi.png"]
])
if populateAssets {
result._testingOnly_populateAssets()
}
return result
}
private func getSubscriptionBadge(populateAssets: Bool = true) -> ProfileBadge {
let result = try! ProfileBadge(jsonDictionary: [
"id": "R_MED",
"category": "donor",
"name": "Subscriber X",
"description": "A subscriber badge!",
"sprites6": ["ldpi.png", "mdpi.png", "hdpi.png", "xhdpi.png", "xxhdpi.png", "xxxhdpi.png"]
])
if populateAssets {
result._testingOnly_populateAssets()
}
return result
}
private func getGiftBadge(populateAssets: Bool = true) -> ProfileBadge {
let result = try! ProfileBadge(jsonDictionary: [
"id": "GIFT",
"category": "donor",
"name": "A Gift",
"description": "A gift badge!",
"sprites6": ["ldpi.png", "mdpi.png", "hdpi.png", "xhdpi.png", "xxhdpi.png", "xxxhdpi.png"]
])
if populateAssets {
result._testingOnly_populateAssets()
}
return result
}
func testBadge() throws {
let badge = getSubscriptionBadge()
let state = State(badge: badge, mode: .subscriptionExpiredBecauseNotRenewed)
XCTAssertIdentical(state.badge, badge)
}
func testTitleText() throws {
let testCases: [(State, String)] = [
(
State(badge: getSubscriptionBadge(),
mode: .subscriptionExpiredBecauseOfChargeFailure(chargeFailure: Subscription.ChargeFailure(code: "insufficient_funds"))),
NSLocalizedString("BADGE_EXPIRED_SUBSCRIPTION_TITLE",
comment: "Title for subscription on the badge expiration sheet.")
),
(
State(badge: getSubscriptionBadge(), mode: .subscriptionExpiredBecauseNotRenewed),
NSLocalizedString("BADGE_EXPIRED_SUBSCRIPTION_TITLE",
comment: "Title for subscription on the badge expiration sheet.")
),
(
State(badge: getSubscriptionBadge(), mode: .boostExpired(hasCurrentSubscription: true)),
NSLocalizedString("BADGE_EXPIRED_BOOST_TITLE",
comment: "Title for boost on the badge expiration sheet.")
),
(
State(badge: getGiftBadge(), mode: .giftBadgeExpired(hasCurrentSubscription: true)),
NSLocalizedString("BADGE_EXPIRED_GIFT_TITLE",
value: "Your Gift Badge Has Expired",
comment: "Title for gift on the badge expiration sheet.")
),
(
State(badge: getGiftBadge(), mode: .giftNotRedeemed(fullName: "")),
NSLocalizedString("GIFT_NOT_REDEEMED_TITLE",
value: "Your Gift Has Expired",
comment: "Title when trying to redeem a gift that's already expired.")
)
]
for (state, expected) in testCases {
XCTAssertEqual(state.titleText, expected)
}
}
func testBody() throws {
let subscriptionBadge = getSubscriptionBadge()
let chargeFailureTestCases: [[String?]: String] = [
["authentication_required"]: NSLocalizedString("DONATION_PAYMENT_ERROR_AUTHENTICATION_REQUIRED",
comment: "Donation payment error for decline failures where authentication is required."),
["approve_with_id"]: NSLocalizedString("DONATION_PAYMENT_ERROR_PAYMENT_CANNOT_BE_AUTHORIZED",
comment: "Donation payment error for decline failures where the payment cannot be authorized."),
["call_issuer"]: NSLocalizedString("DONATION_PAYMENT_ERROR_CALL_ISSUER",
comment: "Donation payment error for decline failures where the user may need to contact their card or bank."),
["card_not_supported"]: NSLocalizedString("DONATION_PAYMENT_ERROR_CARD_NOT_SUPPORTED",
comment: "Donation payment error for decline failures where the card is not supported."),
[
"card_velocity_exceeded", "currency_not_supported", "do_not_honor",
"do_not_try_again", "fraudulent", "generic_decline", "invalid_account",
"invalid_amount", "lost_card", "merchant_blacklist",
"new_account_information_available", "no_action_taken", "not_permitted",
"restricted_card", "revocation_of_all_authorizations",
"revocation_of_authorization", "security_violation", "service_not_allowed",
"stolen_card", "stop_payment_order", "testmode_decline", "transaction_not_allowed",
"try_again_later", "withdrawal_count_limit_exceeded",
"GARBAGE", nil
]: NSLocalizedString("DONATION_PAYMENT_ERROR_OTHER",
comment: "Donation payment error for unspecified decline failures."),
["expired_card"]: NSLocalizedString("DONATION_PAYMENT_ERROR_EXPIRED_CARD",
comment: "Donation payment error for decline failures where the card has expired."),
["incorrect_number"]: NSLocalizedString("DONATION_PAYMENT_ERROR_INCORRECT_CARD_NUMBER",
comment: "Donation payment error for decline failures where the card number is incorrect."),
["incorrect_cvc", "invalid_cvc"]: NSLocalizedString("DONATION_PAYMENT_ERROR_INCORRECT_CARD_VERIFICATION_CODE",
comment: "Donation payment error for decline failures where the card verification code (often called CVV or CVC) is incorrect."),
["insufficient_funds"]: NSLocalizedString("DONATION_PAYMENT_ERROR_INSUFFICIENT_FUNDS",
comment: "Donation payment error for decline failures where the card has insufficient funds."),
["invalid_expiry_month"]: NSLocalizedString("DONATION_PAYMENT_ERROR_INVALID_EXPIRY_MONTH",
comment: "Donation payment error for decline failures where the expiration month on the payment method is incorrect."),
["invalid_expiry_year"]: NSLocalizedString("DONATION_PAYMENT_ERROR_INVALID_EXPIRY_YEAR",
comment: "Donation payment error for decline failures where the expiration year on the payment method is incorrect."),
["invalid_number"]: NSLocalizedString("DONATION_PAYMENT_ERROR_INVALID_NUMBER",
comment: "Donation payment error for decline failures where the card number is incorrect."),
[
"issuer_not_available", "processing_error", "reenter_transaction"
]: NSLocalizedString("DONATION_PAYMENT_ERROR_ISSUER_NOT_AVAILABLE",
comment: "Donation payment error for \"issuer not available\" decline failures. The user should try again or contact their card/bank.")
]
for (codes, expectedSubstring) in chargeFailureTestCases {
for code in codes {
let chargeFailure: Subscription.ChargeFailure
if let code = code {
chargeFailure = Subscription.ChargeFailure(code: code)
} else {
chargeFailure = Subscription.ChargeFailure()
}
let state = State(badge: subscriptionBadge,
mode: .subscriptionExpiredBecauseOfChargeFailure(chargeFailure: chargeFailure))
let body = state.body
XCTAssert(body.text.contains(expectedSubstring))
XCTAssert(body.text.contains(subscriptionBadge.localizedName))
XCTAssertTrue(body.hasLearnMoreLink)
}
}
let otherTestCases: [(State, String, Bool)] = [
(
State(badge: getSubscriptionBadge(), mode: .subscriptionExpiredBecauseNotRenewed),
NSLocalizedString("BADGE_SUBSCRIPTION_EXPIRED_BECAUSE_OF_INACTIVITY_BODY_FORMAT",
comment: "Body of the sheet shown when your subscription is canceled due to inactivity"),
true
),
(
State(badge: getSubscriptionBadge(), mode: .boostExpired(hasCurrentSubscription: false)),
NSLocalizedString("BADGE_EXPIRED_BOOST_BODY",
comment: "String explaining to the user that their boost badge has expired on the badge expiry sheet.")
+ "\n\n"
+ NSLocalizedString("BADGE_EXPIRED_MONTHLY_CALL_TO_ACTION",
comment: "Shown when a non-monthly badge expires to suggest starting a recurring donation."),
false
),
(
State(badge: getSubscriptionBadge(), mode: .boostExpired(hasCurrentSubscription: true)),
NSLocalizedString("BADGE_EXPIRED_BOOST_CURRENT_SUSTAINER_BODY",
comment: "String explaining to the user that their boost badge has expired while they are a current subscription sustainer on the badge expiry sheet."),
false
),
(
State(badge: getGiftBadge(), mode: .giftBadgeExpired(hasCurrentSubscription: false)),
NSLocalizedString(
"BADGE_EXPIRED_GIFT_BODY",
value: "Your gift badge has expired and is no longer available to be displayed on your profile.",
comment: "String explaining to the user that their gift badge has expired. Shown on the badge expiration sheet."
)
+ "\n\n"
+ NSLocalizedString(
"BADGE_EXPIRED_MONTHLY_CALL_TO_ACTION",
comment: "Shown when a non-monthly badge expires to suggest starting a recurring donation."
),
false
),
(
State(badge: getGiftBadge(), mode: .giftBadgeExpired(hasCurrentSubscription: true)),
NSLocalizedString(
"BADGE_EXPIRED_GIFT_BODY",
value: "Your gift badge has expired and is no longer available to be displayed on your profile.",
comment: "String explaining to the user that their gift badge has expired. Shown on the badge expiration sheet."
),
false
),
(
State(badge: getGiftBadge(), mode: .giftNotRedeemed(fullName: "John Doe")),
NSLocalizedString(
"GIFT_NOT_REDEEMED_BODY_FORMAT",
value: "Your gift from %@ has expired and can no longer be redeemed.",
comment: "Shown when trying to redeem a gift that's already expired. Embeds {{contact name}}."
).replacingOccurrences(of: "%@", with: "John Doe"),
false
)
]
for (state, expectedFormat, expectedHasLearnMore) in otherTestCases {
let body = state.body
XCTAssertEqual(body.text, String(format: expectedFormat, state.badge.localizedName))
XCTAssertEqual(body.hasLearnMoreLink, expectedHasLearnMore)
}
}
func testActionButton() throws {
let testCases: [(State, State.ActionButton)] = [
(
State(badge: getSubscriptionBadge(),
mode: .subscriptionExpiredBecauseOfChargeFailure(chargeFailure: Subscription.ChargeFailure(code: "insufficient_funds"))),
State.ActionButton(action: .dismiss, text: CommonStrings.okayButton, hasNotNow: false)
),
(
State(badge: getSubscriptionBadge(), mode: .subscriptionExpiredBecauseNotRenewed),
State.ActionButton(action: .openSubscriptionsView,
text: NSLocalizedString("BADGE_EXPIRED_SUBSCRIPTION_RENEWAL_BUTTON",
comment: "Button text when a badge expires, asking you to renew your subscription"),
hasNotNow: true)
),
(
State(badge: getSubscriptionBadge(), mode: .boostExpired(hasCurrentSubscription: false)),
State.ActionButton(action: .openSubscriptionsView,
text: NSLocalizedString("BADGE_EXPIRED_BOOST_RENEWAL_BUTTON",
comment: "Button title for boost on the badge expiration sheet, used if the user is not already a sustainer."),
hasNotNow: true)
),
(
State(badge: getSubscriptionBadge(), mode: .boostExpired(hasCurrentSubscription: true)),
State.ActionButton(action: .openBoostView,
text: NSLocalizedString("BADGE_EXPIRED_BOOST_RENEWAL_BUTTON_SUSTAINER",
comment: "Button title for boost on the badge expiration sheet, used if the user is already a sustainer."),
hasNotNow: true)
),
(
State(badge: getGiftBadge(), mode: .giftBadgeExpired(hasCurrentSubscription: false)),
State.ActionButton(action: .openSubscriptionsView,
text: NSLocalizedString("BADGE_EXPIRED_RENEWAL_MONTHLY",
value: "Make a Monthly Donation",
comment: "Button title to donate monthly on the badge expiration sheet."),
hasNotNow: true)
),
(
State(badge: getGiftBadge(), mode: .giftBadgeExpired(hasCurrentSubscription: true)),
State.ActionButton(action: .dismiss, text: CommonStrings.okButton, hasNotNow: false)
),
(
State(badge: getGiftBadge(), mode: .giftNotRedeemed(fullName: "")),
State.ActionButton(action: .dismiss, text: CommonStrings.okButton, hasNotNow: false)
)
]
for (state, expectedActionButton) in testCases {
XCTAssertEqual(state.actionButton.action, expectedActionButton.action)
XCTAssertEqual(state.actionButton.text, expectedActionButton.text)
XCTAssertEqual(state.actionButton.hasNotNow, expectedActionButton.hasNotNow)
}
}
}