Skip to content

Commit

Permalink
Rebrand gift cards
Browse files Browse the repository at this point in the history
- Added new and updated design for the gift cards
- Introduced two new SVGs for the gift cards as files that are imported and DOM manipulated in TypeScript in order to dynamically insert the translated text, user message, QR code and gift card value.
- Made login button with smaller width reusable.

Co-authored-by: jug <[email protected]>
Co-authored-by: arm <[email protected]>
Co-authored-by: jat <[email protected]>
  • Loading branch information
3 people committed Dec 12, 2024
1 parent dc61f92 commit 9dc66d0
Show file tree
Hide file tree
Showing 18 changed files with 516 additions and 418 deletions.
88 changes: 88 additions & 0 deletions resources/images/gift-card-no-qr.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions resources/images/gift-card.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/common/gui/base/Checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type CheckboxAttrs = {
label: lazy<string | Children>
checked: boolean
onChecked: (value: boolean) => unknown
class?: string
helpLabel?: TranslationKey | lazy<string>
disabled?: boolean
}
Expand All @@ -24,13 +25,14 @@ export class Checkbox implements Component<CheckboxAttrs> {
const a = vnode.attrs
const helpLabelText = a.helpLabel ? lang.getMaybeLazy(a.helpLabel) : ""
const helpLabel = a.helpLabel ? m(`small.block.content-fg${Checkbox.getBreakClass(helpLabelText)}`, helpLabelText) : []
const userClasses = a.class == null ? "" : " " + a.class
return m(
`.pt`,
{
role: "checkbox",
"aria-checked": String(a.checked),
"aria-disabled": String(a.disabled),
class: getOperatingClasses(a.disabled, "click flash"),
class: getOperatingClasses(a.disabled, "click flash") + userClasses,
onclick: (e: MouseEvent) => {
if (e.target !== this._domInput) {
this.toggle(e, a) // event is bubbling in IE besides we invoke e.stopPropagation()
Expand Down
11 changes: 11 additions & 0 deletions src/common/gui/base/GuiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { LoginController } from "../../api/main/LoginController.js"
import { client } from "../../misc/ClientDetector.js"
import type { Contact } from "../../api/entities/tutanota/TypeRefs.js"
import { isColorLight } from "./Color.js"
import QRCode from "qrcode-svg"
import { htmlSanitizer } from "../../misc/HtmlSanitizer.js"

export type dropHandler = (dragData: string) => void
// not all browsers have the actual button as e.currentTarget, but all of them send it as a second argument (see https://github.com/tutao/tutanota/issues/1110)
Expand Down Expand Up @@ -247,3 +249,12 @@ export function getContactTitle(contact: Contact) {
export function colorForBg(color: string): string {
return isColorLight(color) ? "black" : "white"
}

/**
* Generates a sanitised QR Code in SVG form
* @return the SVG element of the generated QR code as a `string`
*/
export function generateQRCode(options: QRCode.Options): string {
const svg = new QRCode(options).svg()
return htmlSanitizer.sanitizeSVG(svg).html
}
6 changes: 6 additions & 0 deletions src/common/gui/main-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ styles.registerStyle("main", () => {
".smaller": {
"font-size": px(size.font_size_smaller),
},
".normal-font-size": {
"font-size": px(size.font_size_base),
},
".b": {
"font-weight": "bold",
},
Expand Down Expand Up @@ -1752,6 +1755,9 @@ styles.registerStyle("main", () => {
width: "100%",
"border-radius": px(size.border_radius),
},
".small-login-button": {
width: "260px",
},
".button-content": {
height: px(size.button_height),
"min-width": px(size.button_height),
Expand Down
29 changes: 12 additions & 17 deletions src/common/settings/login/secondfactor/SecondFactorEditModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import { validateWebauthnDisplayName, WebauthnClient } from "../../../misc/2fa/w
import { TotpSecret } from "@tutao/tutanota-crypto"
import { assertNotNull, LazyLoaded, neverNull } from "@tutao/tutanota-utils"
import { isApp } from "../../../api/common/Env.js"
import { htmlSanitizer } from "../../../misc/HtmlSanitizer.js"
import { LanguageViewModel, TranslationKey } from "../../../misc/LanguageViewModel.js"
import { SecondFactorType } from "../../../api/common/TutanotaConstants.js"
import { ProgrammingError } from "../../../api/common/error/ProgrammingError.js"
import QRCode from "qrcode-svg"
import { LoginFacade } from "../../../api/worker/facades/LoginFacade.js"
import { UserError } from "../../../api/main/UserError.js"
import { generateQRCode } from "../../../gui/base/GuiUtils.js"

export const enum VerificationStatus {
Initial = "Initial",
Expand Down Expand Up @@ -62,21 +61,17 @@ export class SecondFactorEditModel {
this.setDefaultNameIfNeeded()
this.otpInfo = new LazyLoaded(async () => {
const url = await this.getOtpAuthUrl(this.totpKeys.readableKey)
let totpQRCodeSvg

if (!isApp()) {
let qrcodeGenerator = new QRCode({
height: 150,
width: 150,
content: url,
padding: 2,
// We don't want <xml> around the content, we actually enforce <svg> namespace, and we want it to be parsed as such.
xmlDeclaration: false,
})
totpQRCodeSvg = htmlSanitizer.sanitizeSVG(qrcodeGenerator.svg()).html
} else {
totpQRCodeSvg = null
}

const totpQRCodeSvg = isApp()
? null
: generateQRCode({
height: 150,
width: 150,
content: url,
padding: 2,
// We don't want <xml> around the content, we actually enforce <svg> namespace, and we want it to be parsed as such.
xmlDeclaration: false,
})

return {
qrCodeSvg: totpQRCodeSvg,
Expand Down
17 changes: 5 additions & 12 deletions src/common/subscription/InvoiceAndPaymentDataPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,18 +209,11 @@ export class InvoiceAndPaymentDataPage implements WizardPageN<UpgradeSubscriptio
]),
m(
".flex-center.full-width.pt-l",
m(
"",
{
style: {
width: "260px",
},
},
m(LoginButton, {
label: "next_action",
onclick: onNextClick,
}),
),
m(LoginButton, {
label: "next_action",
class: "small-login-button",
onclick: onNextClick,
}),
),
]
: null,
Expand Down
17 changes: 5 additions & 12 deletions src/common/subscription/UpgradeConfirmSubscriptionPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,18 +187,11 @@ export class UpgradeConfirmSubscriptionPage implements WizardPageN<UpgradeSubscr
),
m(
".flex-center.full-width.pt-l",
m(
"",
{
style: {
width: "260px",
},
},
m(LoginButton, {
label: "buy_action",
onclick: () => this.upgrade(attrs.data),
}),
),
m(LoginButton, {
label: "buy_action",
class: "small-login-button",
onclick: () => this.upgrade(attrs.data),
}),
),
]
}
Expand Down
39 changes: 16 additions & 23 deletions src/common/subscription/UpgradeCongratulationsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,30 +38,23 @@ export class UpgradeCongratulationsPage implements WizardPageN<UpgradeSubscripti
: null,
m(
".flex-center.full-width.pt-l",
m(
"",
{
style: {
width: "260px",
},
m(LoginButton, {
label: "ok_action",
class: "small-login-button",
onclick: () => {
if (attrs.data.type === PlanType.Free) {
const recoveryConfirmationStageFree = this.__signupFreeTest?.getStage(5)

recoveryConfirmationStageFree?.setMetric({
name: "switchedFromPaid",
value: (this.__signupPaidTest?.isStarted() ?? false).toString(),
})
recoveryConfirmationStageFree?.complete()
}

this.close(attrs.data, this.dom)
},
m(LoginButton, {
label: "ok_action",
onclick: () => {
if (attrs.data.type === PlanType.Free) {
const recoveryConfirmationStageFree = this.__signupFreeTest?.getStage(5)

recoveryConfirmationStageFree?.setMetric({
name: "switchedFromPaid",
value: (this.__signupPaidTest?.isStarted() ?? false).toString(),
})
recoveryConfirmationStageFree?.complete()
}

this.close(attrs.data, this.dom)
},
}),
),
}),
),
]
}
Expand Down
17 changes: 9 additions & 8 deletions src/common/subscription/giftcards/GiftCardMessageEditorField.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import m, { Children, Component, Vnode } from "mithril"
import { lang } from "../../misc/LanguageViewModel"
import { assertNotNull } from "@tutao/tutanota-utils"
import { px, size } from "../../gui/size.js"

const GIFT_CARD_MESSAGE_COLS = 26
export const GIFT_CARD_MESSAGE_COLS = 26
const GIFT_CARD_MESSAGE_HEIGHT = 5
type GiftCardMessageEditorFieldAttrs = {
message: string
onMessageChanged: (message: string) => void
cols?: number
rows?: number
}

/**
* A text area that allows you to edit some text that is limited to fit within a certain rows/columns boundary
*/
export class GiftCardMessageEditorField implements Component<GiftCardMessageEditorFieldAttrs> {
textAreaDom: HTMLTextAreaElement | null = null
isActive: boolean = false
private textAreaDom: HTMLTextAreaElement | null = null
private isActive: boolean = false

view(vnode: Vnode<GiftCardMessageEditorFieldAttrs>): Children {
const a = vnode.attrs
return m("", [
m(".small.mt-form.i", lang.get("yourMessage_label")),
m("textarea.monospace.center.overflow-hidden.resize-none" + (this.isActive ? ".editor-border-active" : ".editor-border"), {
return m("label.small.mt-form.i.flex-center.flex-column", [
// Cannot wrap the label in a span to apply the `small` class as it will break screen readers
lang.get("yourMessage_label"),
m("textarea.monospace.normal-font-size.overflow-hidden.resize-none" + (this.isActive ? ".editor-border-active" : ".editor-border"), {
wrap: "hard",
cols: a.cols || GIFT_CARD_MESSAGE_COLS,
rows: a.rows || GIFT_CARD_MESSAGE_HEIGHT,
Expand All @@ -48,7 +49,7 @@ export class GiftCardMessageEditorField implements Component<GiftCardMessageEdit

a.onMessageChanged(textAreaDom.value)

// the cursor gets pushed to the end when we chew up tailing characters so we put it back where it started in that case
// the cursor gets pushed to the end when we chew up tailing characters, so we put it back where it started in that case
if (textAreaDom.selectionStart - origStart > 1) {
textAreaDom.selectionStart = origStart
textAreaDom.selectionEnd = origEnd
Expand Down
Loading

0 comments on commit 9dc66d0

Please sign in to comment.