Skip to content

Commit

Permalink
Add usage tests to AppStore payment
Browse files Browse the repository at this point in the history
  • Loading branch information
murilopereirame authored and tutao-mac committed May 30, 2024
1 parent 860aaf6 commit 63106fa
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 26 deletions.
9 changes: 7 additions & 2 deletions src/api/common/TutanotaConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ContactSocialId, MailFolder } from "../entities/tutanota/TypeRefs.js"
import { isApp, isElectronClient, isIOSApp } from "./Env"
import type { Country } from "./CountryList"
import { ProgrammingError } from "./error/ProgrammingError"
import { AppStorePaymentPicker } from "../../misc/AppStorePaymentPicker.js"

export const MAX_NBR_MOVE_DELETE_MAIL_SERVICE = 50

Expand Down Expand Up @@ -290,8 +291,12 @@ export enum PaymentMethodType {
AppStore = "5",
}

export function defaultPaymentMethod(): PaymentMethodType {
return isIOSApp() ? PaymentMethodType.AppStore : PaymentMethodType.CreditCard
export async function getDefaultPaymentMethod(appStorePaymentPicker: AppStorePaymentPicker): Promise<PaymentMethodType> {
if (isIOSApp() && (await appStorePaymentPicker.shouldEnableAppStorePayment(null))) {
return PaymentMethodType.AppStore
}

return PaymentMethodType.CreditCard
}

export const PaymentMethodTypeToName = reverse(PaymentMethodType)
Expand Down
3 changes: 3 additions & 0 deletions src/api/main/MainLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ import { MobileContactsFacade } from "../../native/common/generatedipc/MobileCon
import { PermissionError } from "../common/error/PermissionError.js"
import { WebMobileFacade } from "../../native/main/WebMobileFacade.js"
import { MobilePaymentsFacade } from "../../native/common/generatedipc/MobilePaymentsFacade.js"
import { AppStorePaymentPicker } from "../../misc/AppStorePaymentPicker.js"

assertMainOrNode()

Expand Down Expand Up @@ -165,6 +166,7 @@ class MainLocator {
operationProgressTracker!: OperationProgressTracker
infoMessageHandler!: InfoMessageHandler
Const!: Record<string, any>
appStorePaymentPicker!: AppStorePaymentPicker

private nativeInterfaces: NativeInterfaces | null = null
private exposedNativeInterfaces: ExposedNativeInterface | null = null
Expand Down Expand Up @@ -776,6 +778,7 @@ class MainLocator {
this.contactModel = new ContactModel(this.searchFacade, this.entityClient, this.logins, this.eventController)
this.minimizedMailModel = new MinimizedMailEditorViewModel()
this.usageTestController = new UsageTestController(this.usageTestModel)
this.appStorePaymentPicker = new AppStorePaymentPicker()
}

readonly calendarModel: () => Promise<CalendarModel> = lazyMemoized(async () => {
Expand Down
35 changes: 35 additions & 0 deletions src/misc/AppStorePaymentPicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { UsageTest, UsageTestController } from "@tutao/tutanota-usagetests"
import { PaymentMethodType } from "../api/common/TutanotaConstants.js"
import { StorageBehavior, UsageTestModel } from "./UsageTestModel.js"
import { locator } from "../api/main/MainLocator.js"
import { LazyLoaded, lazy } from "@tutao/tutanota-utils"

export class AppStorePaymentPicker {
private appStorePaymentUsageTest: lazy<UsageTest> = () => {
return locator.usageTestController.getTest("payment.appstore")
}

async shouldEnableAppStorePayment(currentPaymentMethod: PaymentMethodType | null): Promise<boolean> {
// Prevent users with AppStorePayment from losing the ability to modify their plan
if (currentPaymentMethod === PaymentMethodType.AppStore) {
return true
}

const appStorePaymentUsageTest = this.appStorePaymentUsageTest()
const shouldEnable = appStorePaymentUsageTest.getVariant({
[0]: () => false,
[1]: () => true,
})

if (shouldEnable) {
await appStorePaymentUsageTest.getStage(0).complete()
}

return shouldEnable
}

async markSubscribedStageAsComplete() {
const appStorePaymentUsageTest = this.appStorePaymentUsageTest()
await appStorePaymentUsageTest.getStage(1).complete()
}
}
2 changes: 1 addition & 1 deletion src/settings/SettingsView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ export class SettingsView extends BaseTopLevelView implements TopLevelView<Setti
"adminSubscription_action",
() => BootIcons.Premium,
"subscription",
() => new SubscriptionViewer(currentPlanType, isIOSApp() ? locator.mobilePaymentsFacade : null),
() => new SubscriptionViewer(currentPlanType, isIOSApp() ? locator.mobilePaymentsFacade : null, locator.appStorePaymentPicker),
undefined,
),
)
Expand Down
6 changes: 4 additions & 2 deletions src/subscription/InvoiceAndPaymentDataPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { InvoiceDataInput, InvoiceDataInputLocation } from "./InvoiceDataInput"
import { PaymentMethodInput } from "./PaymentMethodInput"
import stream from "mithril/stream"
import Stream from "mithril/stream"
import type { InvoiceData, PaymentData, PlanType } from "../api/common/TutanotaConstants"
import { getDefaultPaymentMethod, InvoiceData, PaymentData } from "../api/common/TutanotaConstants"
import { getClientType, Keys, PaymentDataResultType, PaymentMethodType, PaymentMethodTypeToName } from "../api/common/TutanotaConstants"
import { showProgressDialog } from "../gui/dialogs/ProgressDialog"
import type { AccountingInfo, Braintree3ds2Request } from "../api/entities/sys/TypeRefs.js"
Expand Down Expand Up @@ -95,7 +95,8 @@ export class InvoiceAndPaymentDataPage implements WizardPageN<UpgradeSubscriptio
)
}
})
.then(() => {
.then(() => getDefaultPaymentMethod(locator.appStorePaymentPicker))
.then((defaultPaymentMethod: PaymentMethodType) => {
this._invoiceDataInput = new InvoiceDataInput(data.options.businessUse(), data.invoiceData, InvoiceDataInputLocation.InWizard)
let payPalRequestUrl = getLazyLoadedPayPalUrl()

Expand All @@ -108,6 +109,7 @@ export class InvoiceAndPaymentDataPage implements WizardPageN<UpgradeSubscriptio
this._invoiceDataInput.selectedCountry,
neverNull(data.accountingInfo),
payPalRequestUrl,
defaultPaymentMethod,
)
this._availablePaymentMethods = this._paymentMethodInput.getVisiblePaymentMethods()

Expand Down
13 changes: 10 additions & 3 deletions src/subscription/PaymentDataDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ import { PaymentMethodInput } from "./PaymentMethodInput"
import { updatePaymentData } from "./InvoiceAndPaymentDataPage"
import { px } from "../gui/size"
import { showProgressDialog } from "../gui/dialogs/ProgressDialog"
import { PaymentMethodType } from "../api/common/TutanotaConstants"
import { getDefaultPaymentMethod, PaymentMethodType } from "../api/common/TutanotaConstants"
import { assertNotNull, neverNull } from "@tutao/tutanota-utils"
import type { AccountingInfo, Customer } from "../api/entities/sys/TypeRefs.js"
import { DropDownSelector } from "../gui/base/DropDownSelector.js"
import { asPaymentInterval } from "./PriceUtils.js"
import { getLazyLoadedPayPalUrl } from "./SubscriptionUtils.js"
import { formatNameAndAddress } from "../api/common/utils/CommonFormatter.js"
import { locator } from "../api/main/MainLocator.js"

/**
* @returns {boolean} true if the payment data update was successful
*/
export function show(customer: Customer, accountingInfo: AccountingInfo, price: number): Promise<boolean> {
export async function show(customer: Customer, accountingInfo: AccountingInfo, price: number, defaultPaymentMethod: PaymentMethodType): Promise<boolean> {
const payPalRequestUrl = getLazyLoadedPayPalUrl()
const invoiceData = {
invoiceAddress: formatNameAndAddress(accountingInfo.invoiceName, accountingInfo.invoiceAddress),
Expand All @@ -29,7 +30,13 @@ export function show(customer: Customer, accountingInfo: AccountingInfo, price:
businessUse: stream(assertNotNull(customer.businessUse)),
paymentInterval: stream(asPaymentInterval(accountingInfo.paymentInterval)),
}
const paymentMethodInput = new PaymentMethodInput(subscriptionOptions, stream(invoiceData.country), neverNull(accountingInfo), payPalRequestUrl)
const paymentMethodInput = new PaymentMethodInput(
subscriptionOptions,
stream(invoiceData.country),
neverNull(accountingInfo),
payPalRequestUrl,
defaultPaymentMethod,
)
const availablePaymentMethods = paymentMethodInput.getVisiblePaymentMethods()

let selectedPaymentMethod = accountingInfo.paymentMethod as PaymentMethodType
Expand Down
5 changes: 3 additions & 2 deletions src/subscription/PaymentMethodInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { TranslationKey } from "../misc/LanguageViewModel"
import { lang } from "../misc/LanguageViewModel"
import type { Country } from "../api/common/CountryList"
import { CountryType } from "../api/common/CountryList"
import { defaultPaymentMethod, PaymentData, PaymentMethodType } from "../api/common/TutanotaConstants"
import { PaymentData, PaymentMethodType } from "../api/common/TutanotaConstants"
import { PayPalLogo } from "../gui/base/icons/Icons"
import { LazyLoaded, noOp, promiseMap } from "@tutao/tutanota-utils"
import { showProgressDialog } from "../gui/dialogs/ProgressDialog"
Expand Down Expand Up @@ -39,6 +39,7 @@ export class PaymentMethodInput {
selectedCountry: Stream<Country | null>,
accountingInfo: AccountingInfo,
payPalRequestUrl: LazyLoaded<string>,
defaultPaymentMethod: PaymentMethodType,
) {
this._selectedCountry = selectedCountry
this._subscriptionOptions = subscriptionOptions
Expand All @@ -63,7 +64,7 @@ export class PaymentMethodInput {
}).then(noOp)
}

this._selectedPaymentMethod = defaultPaymentMethod()
this._selectedPaymentMethod = defaultPaymentMethod
}

oncreate() {
Expand Down
22 changes: 14 additions & 8 deletions src/subscription/PaymentViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Icons } from "../gui/base/icons/Icons"
import { ColumnWidth, Table, TableLineAttrs } from "../gui/base/Table.js"
import { ButtonType } from "../gui/base/Button.js"
import { formatDate } from "../misc/Formatter"
import { getPaymentMethodType, NewPaidPlans, PaymentMethodType, PostingType } from "../api/common/TutanotaConstants"
import { getDefaultPaymentMethod, getPaymentMethodType, NewPaidPlans, PaymentMethodType, PostingType } from "../api/common/TutanotaConstants"
import { BadGatewayError, LockedError, PreconditionFailedError, TooManyRequestsError } from "../api/common/error/RestError"
import { Dialog, DialogType } from "../gui/base/Dialog"
import { getByAbbreviation } from "../api/common/CountryList"
Expand Down Expand Up @@ -185,15 +185,21 @@ export class PaymentViewer implements UpdatableSettingsViewer {
Number(neverNull(priceServiceReturn.currentPriceNextPeriod).price),
)
}),
).then((price) => {
return PaymentDataDialog.show(neverNull(this._customer), neverNull(this._accountingInfo), price).then((success) => {
if (success) {
if (this._isPayButtonVisible()) {
return this._showPayDialog(this._amountOwed())
)
.then((price) =>
getDefaultPaymentMethod(locator.appStorePaymentPicker).then((paymentMethod) => {
return { price, paymentMethod }
}),
)
.then(({ price, paymentMethod }) => {
return PaymentDataDialog.show(neverNull(this._customer), neverNull(this._accountingInfo), price, paymentMethod).then((success) => {
if (success) {
if (this._isPayButtonVisible()) {
return this._showPayDialog(this._amountOwed())
}
}
}
})
})
})
}

_renderPostings(postingExpanded: Stream<boolean>): Children {
Expand Down
16 changes: 14 additions & 2 deletions src/subscription/SubscriptionViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { EntityUpdateData, isUpdateForTypeRef } from "../api/common/utils/Entity
import { showProgressDialog } from "../gui/dialogs/ProgressDialog"
import { MobilePaymentsFacade } from "../native/common/generatedipc/MobilePaymentsFacade"
import { MobilePaymentSubscriptionOwnership } from "../native/common/generatedipc/MobilePaymentSubscriptionOwnership"
import { AppStorePaymentPicker } from "../misc/AppStorePaymentPicker.js"

assertMainOrNode()
const DAY = 1000 * 60 * 60 * 24
Expand Down Expand Up @@ -102,7 +103,11 @@ export class SubscriptionViewer implements UpdatableSettingsViewer {
private _giftCards: Map<Id, GiftCard>
private _giftCardsExpanded: Stream<boolean>

constructor(currentPlanType: PlanType, private readonly mobilePaymentsFacade: MobilePaymentsFacade | null) {
constructor(
currentPlanType: PlanType,
private readonly mobilePaymentsFacade: MobilePaymentsFacade | null,
private readonly appStorePaymentPicker: AppStorePaymentPicker,
) {
this.currentPlanType = currentPlanType
const isPremiumPredicate = () => locator.logins.getUserController().isPremiumAccount()

Expand Down Expand Up @@ -263,6 +268,13 @@ export class SubscriptionViewer implements UpdatableSettingsViewer {

private async handleUpgradeSubscription() {
if (isIOSApp()) {
const currentPaymentType = this._accountingInfo ? getPaymentMethodType(this._accountingInfo) : null
const shouldEnableiOSPayment = await this.appStorePaymentPicker.shouldEnableAppStorePayment(currentPaymentType)

if (!shouldEnableiOSPayment) {
return Dialog.message("notAvailableInApp_msg")
}

const hasOngoingAppStoreSubsciption = await hasAppStoreOngoingSubscription(null)

if (hasOngoingAppStoreSubsciption !== MobilePaymentSubscriptionOwnership.NoSubscription) {
Expand All @@ -274,7 +286,7 @@ export class SubscriptionViewer implements UpdatableSettingsViewer {
}
}

showUpgradeWizard(locator.logins)
return showUpgradeWizard(locator.logins)
}

private async handleAppStoreSubscriptionChange() {
Expand Down
8 changes: 7 additions & 1 deletion src/subscription/UpgradeConfirmSubscriptionPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export class UpgradeConfirmSubscriptionPage implements WizardPageN<UpgradeSubscr
return false
}

return updatePaymentData(
const success = await updatePaymentData(
data.options.paymentInterval(),
data.invoiceData,
data.paymentData,
Expand All @@ -123,6 +123,12 @@ export class UpgradeConfirmSubscriptionPage implements WizardPageN<UpgradeSubscr
null,
data.accountingInfo!,
)

if (success) {
await locator.appStorePaymentPicker.markSubscribedStageAsComplete()
}

return success
}

private renderConfirmSubscription(attrs: WizardPageAttrs<UpgradeSubscriptionData>) {
Expand Down
11 changes: 6 additions & 5 deletions src/subscription/UpgradeSubscriptionWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AccountingInfo, Customer } from "../api/entities/sys/TypeRefs.js"
import {
AvailablePlans,
AvailablePlanType,
defaultPaymentMethod,
getDefaultPaymentMethod,
getPaymentMethodType,
InvoiceData,
NewPaidPlans,
Expand Down Expand Up @@ -86,7 +86,7 @@ export async function showUpgradeWizard(logins: LoginController, acceptedPlans:
vatNumber: accountingInfo.invoiceVatIdNo, // only for EU countries otherwise empty
},
paymentData: {
paymentMethod: getPaymentMethodType(accountingInfo) || defaultPaymentMethod(),
paymentMethod: getPaymentMethodType(accountingInfo) || (await getDefaultPaymentMethod(locator.appStorePaymentPicker)),
creditCardData: null,
},
price: "",
Expand Down Expand Up @@ -147,12 +147,13 @@ export async function loadSignupWizard(
const featureListProvider = await FeatureListProvider.getInitializedInstance(domainConfig)

let hasAppStoreSubscription = MobilePaymentSubscriptionOwnership.NoSubscription

let enableAppStoreSubscription = false
if (isIOSApp()) {
hasAppStoreSubscription = await hasAppStoreOngoingSubscription(null)
enableAppStoreSubscription = await locator.appStorePaymentPicker.shouldEnableAppStorePayment(null)
}

if (hasAppStoreSubscription !== MobilePaymentSubscriptionOwnership.NoSubscription) {
if (hasAppStoreSubscription !== MobilePaymentSubscriptionOwnership.NoSubscription || !enableAppStoreSubscription) {
acceptedPlans = acceptedPlans.filter((plan) => plan === PlanType.Free)
}

Expand All @@ -167,7 +168,7 @@ export async function loadSignupWizard(
vatNumber: "", // only for EU countries otherwise empty
},
paymentData: {
paymentMethod: defaultPaymentMethod(),
paymentMethod: await getDefaultPaymentMethod(locator.appStorePaymentPicker),
creditCardData: null,
},
price: "",
Expand Down

0 comments on commit 63106fa

Please sign in to comment.