Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Credit Card token UI/UX #504

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend_vue/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare module 'vue' {
export interface GlobalComponents {
BaseButton: typeof import('./src/components/base/BaseButton.vue')['default']
BaseCodeSnippet: typeof import('./src/components/base/BaseCodeSnippet.vue')['default']
BaseContentBlock: typeof import('src/components/base/BaseContentBlock.vue')['default']
BaseCopyButton: typeof import('./src/components/base/BaseCopyButton.vue')['default']
BaseFormSelect: typeof import('./src/components/base/BaseFormSelect.vue')['default']
BaseFormTextField: typeof import('./src/components/base/BaseFormTextField.vue')['default']
Expand Down
Binary file added frontend_vue/src/assets/cc-background-AMEX.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend_vue/src/assets/fonts/ocr-a-extended.ttf
Binary file not shown.
9 changes: 9 additions & 0 deletions frontend_vue/src/assets/fonts/typography.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,12 @@
url('./fa-solid-900.woff2') format('woff2'),
url('./fa-solid-900.ttf') format('truetype');
}

@font-face {
font-family: 'OCR A Extended';
font-style: normal;
font-weight: 900;
font-display: block;
src:
url('./ocr-a-extended.ttf') format('truetype'),
}
Binary file added frontend_vue/src/assets/token_icons/cc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion frontend_vue/src/components/ManageToken.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,9 @@ async function fetchTokenData() {
}

async function loadComponent() {
const toke = 'cc'
dynamicComponent.value = await defineAsyncComponent(
() => import(`@/components/tokens/${getTokenType.value}/ManageToken.vue`)
() => import(`@/components/tokens/${toke}/ManageToken.vue`)
);
}

Expand Down
13 changes: 7 additions & 6 deletions frontend_vue/src/components/ModalToken.vue
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,13 @@ async function handleGenerateToken(formValues: BaseFormValuesType) {
...formValues,
token_type: getTokenType.value,
});
if (res.status !== 200) {
isLoadngSubmit.value = false;
triggerSubmit.value = false;
errorMessage.value = res.data.error_message;
return (isGenerateTokenError.value = true);
}
console.log(res);
// if (res.status !== 200) {
// isLoadngSubmit.value = false;
// triggerSubmit.value = false;
// errorMessage.value = res.data.error_message;
// return (isGenerateTokenError.value = true);
// }
/* AZURE CONFIG Exception handler */
/* Overwrite backend response for Azure ID Config token type */
/* It's needed as Azure ID Config returns CSS Cloned Site */
Expand Down
51 changes: 51 additions & 0 deletions frontend_vue/src/components/base/BaseContentBlock.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { mount } from '@vue/test-utils';
import BaseContentBlock from '@/components/base/BaseContentBlock.vue';
import BaseCopyButton from './BaseCopyButton.vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

describe('BaseContentBlock', () => {
it('renders text correctly', () => {
const label = 'Card Name';
const text = 'John Doe';
const iconName = 'credit-card';
const wrapper = mount(BaseContentBlock, {
props: { label, text, iconName, copyContent: true },
global: {
stubs: { BaseCopyButton, FontAwesomeIcon },
},
});

expect(wrapper.text()).toContain(label);
expect(wrapper.text()).toContain(text);
});

it('emits click event when button is clicked', async () => {
const label = 'Card Name';
const text = 'John Doe';
const iconName = 'credit-card';
const wrapper = mount(BaseContentBlock, {
props: { label, text, iconName, copyContent: true },
global: {
stubs: { BaseCopyButton, FontAwesomeIcon },
},
});

await wrapper.find('button').trigger('click');

expect(wrapper.emitted('click')).toBeTruthy();
});

it('does not render copy if copyContent is false', async () => {
const label = 'Card Name';
const text = 'John Dude';
const iconName = 'credit-card';
const wrapper = mount(BaseContentBlock, {
props: { label, text, iconName, copyContent: false },
global: {
stubs: { BaseCopyButton },
},
});

expect(wrapper.find('button').exists()).toBe(false);;
});
});
47 changes: 47 additions & 0 deletions frontend_vue/src/components/base/BaseContentBlock.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<template>
<span
class="content-block flex items-center justify-between gap-8 sm:gap-24 py-8 px-[1em] text-grey-500 border border-grey-200 rounded-xl hover:bg-white hover:text-grey-700">
<div class="flex items-center">
<font-awesome-icon class="text-grey-400 mr-8 sm:mr-[.75em] text-lg sm:text-xl" aria-hidden="true" :icon="iconName" />
<div>
<span class="block text-xs font-medium text-grey-800 mb-1.5">{{ props.label }}:</span>
<span class="block" :class="{ copied }">{{ props.text }}</span>
</div>
</div>
<BaseCopyButton v-if="copyContent" :content="props.text" class="ring-white ring-4" @click="handleCopyText()" />
</span>
</template>

<script setup lang="ts">
import { ref } from "vue";

const props = defineProps<{
label: string;
text: string;
copyContent: boolean;
iconName: string;
}>();

const copied = ref(false)

const handleCopyText = () => {
copied.value = true;
setTimeout(() => {
copied.value = false;
}, 1750);
}
</script>
<style lang="scss" scoped>
.content-block {
button {
opacity: 0;
}
&:hover button {
opacity: 1;
}
}
span.copied {
background-color: #0393b3;
color: white;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`BaseCopyButton > render correctly 1`] = `
"<button data-v-e7e863f3="" class="h-[2rem] w-[2rem] font-semibold text-white rounded-full bg-green hover:bg-green-300 transition duration-100" aria-label="Copy to clipboard">
<transition-stub data-v-e7e863f3="" name="fade" mode="out-in" appear="false" persisted="false" css="true"><svg data-v-e7e863f3="" class="svg-inline--fa fa-copy" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="copy" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path class="" fill="currentColor" d="M208 0H332.1c12.7 0 24.9 5.1 33.9 14.1l67.9 67.9c9 9 14.1 21.2 14.1 33.9V336c0 26.5-21.5 48-48 48H208c-26.5 0-48-21.5-48-48V48c0-26.5 21.5-48 48-48zM48 128h80v64H64V448H256V416h64v48c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V176c0-26.5 21.5-48 48-48z"></path>
</svg></transition-stub>
<!--v-if-->
</button>"
`;
2 changes: 1 addition & 1 deletion frontend_vue/src/components/icons/TokenIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<BaseSkeletonLoader
v-if="isLoading"
type="circle"
class="ratio-[1:1] w-full h-full"
class="ratio-[1:1] w-full h-[96px]"
/>
<img
v-if="!isLoading"
Expand Down
28 changes: 28 additions & 0 deletions frontend_vue/src/components/tokens/cc/ActivatedToken.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<base-message-box
v-if="!tokenData"
class="mt-24" variant="info"
:message="`Credit card token not available yet'`" />
<TokenDisplay v-else :token-data="tokenData" />
<base-message-box
class="mt-24" variant="info"
:message="`If the card number is ever used in an authorization, the transaction will be declined, but you will be alerted.'`" />
<p class="mt-24 text-sm"></p>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import TokenDisplay from './TokenDisplay.vue';
import type { CCtokenDataType } from '@/components/tokens/types.ts';

const props = defineProps<{
tokenData: CCtokenDataType;
}>();

const tokenData = ref({
card_name: props.tokenData.card_name || 'Paul Ndegwa Gichuki',
card_number: props.tokenData.card_number || '0000 0000 0000 0000',
expiry: props.tokenData.expiry || '11/2027',
cvc: props.tokenData.cvc || '344',
});
</script>
8 changes: 8 additions & 0 deletions frontend_vue/src/components/tokens/cc/GenerateTokenForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<GenerateTokenSettingsNotifications
memo-helper-example="Credit Card placed in payment card database" />
</template>

<script setup lang="ts">
import GenerateTokenSettingsNotifications from '@/components/ui/GenerateTokenSettingsNotifications.vue';
</script>
29 changes: 29 additions & 0 deletions frontend_vue/src/components/tokens/cc/ManageToken.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<div v-if="!tokenInfo">Error loading</div>
<TokenDisplay v-else :token-data="tokenInfo" />
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import TokenDisplay from './TokenDisplay.vue';
import type { ManageTokenBackendType } from '@/components/tokens/types.ts';

const props = defineProps<{
tokenData: ManageTokenBackendType;
}>();

// TODO: these fields are coming empty from the backend
// const tokenData = ref({
// card_name: props.tokenData.card_name || 'Paul Ndegwa Gichuki',
// card_number: props.tokenData.card_number || '0000 0000 0000 0000 0000',
// expiry: props.tokenData.expiry || '11/27',
// cvc: props.tokenData.cvc || '344',
// });

const tokenInfo = ref({
card_name: 'Paul Ndegwa Gichuki',
card_number:'0000 0000 0000 0000',
expiry: '11/2027',
cvc: '344',
});
</script>
30 changes: 30 additions & 0 deletions frontend_vue/src/components/tokens/cc/TokenDisplay.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<div class="flex flex-col items-center">
<CreditCardToken
:token-data="tokenData" />
<base-button
class="mt-24"
:href="href"
:download="download"
@click="handleDownloadCC()">
Download Credit Card
</base-button>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import CreditCardToken from '@/components/ui/CreditCardToken.vue';
import type { CCtokenDataType } from '@/components/tokens/types.ts';

const props = defineProps<{
tokenData: CCtokenDataType;
}>();

const href = ref('download?fmt=cc'+'&token=mwnioj2ijoij2223'+'&auth=wkoowmwojojoow')
const download = ref(props.tokenData.card_name.concat('_'))

const handleDownloadCC = () => {
console.log('Download CC')
}
</script>
5 changes: 5 additions & 0 deletions frontend_vue/src/components/tokens/cc/howToUse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const howToUse = [
'You can use any name or address in association with the card number and CVC',
'Stick this in a database that stores payment information and get alerted if it is ever breached.',
'Put it in a document with other personal information regarding e.g., travel preferences in case someone snoops around your computer'
];
7 changes: 7 additions & 0 deletions frontend_vue/src/components/tokens/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,10 @@ export type HistoryTokenBackendType = {
canarydrop: CanaryDropType;
google_api_key: string | null;
};

export type CCtokenDataType = {
card_name: string;
card_number: string;
expiry: string;
cvc: string;
};
42 changes: 42 additions & 0 deletions frontend_vue/src/components/ui/CreditCardToken.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<template>
<div id="cc-card" class="w-[280px] h-[11em] sm:w-[20.5em] sm:h-[13em] relative m-auto text-white">
<span class="absolute top-[50px] sm:top-[65px] left-[20px] sm:left-[25px]">{{ props.tokenData.card_name }}</span>
<span class="absolute top-[75px] sm:top-[90px] left-[20px] sm:left-[25px]">{{ props.tokenData.card_number }}</span>
<span class="absolute top-[135px] sm:top-[160px] left-[20px] sm:left-[25px]">{{ props.tokenData.expiry }}</span>
<span class="absolute top-[135px] sm:top-[160px] left-[135px] sm:left-[165px]">{{ props.tokenData.cvc }}</span>
</div>
<div class="grid grid-cols-6 p-16 text-sm grid-flow-row-dense gap-8 mt-24 items-center border border-grey-200 rounded-xl shadow-solid-shadow-grey">
<BaseContentBlock
class="col-span-6 sm:col-span-4" :label="'Card Name'" :text="props.tokenData.card_name" :icon-name="'id-card'" copy-content />
<BaseContentBlock
class="col-span-6 sm:col-span-4" :label="'Card Number'" :text="props.tokenData.card_number" :icon-name="'credit-card'" copy-content />
<BaseContentBlock
class="col-span-3 sm:col-span-2" :label="'Expires'" :text="props.tokenData.expiry" :icon-name="'calendar-day'" copy-content />
<BaseContentBlock
class="col-span-3 sm:col-span-2" :label="'CVC'" :text="props.tokenData.cvc" :icon-name="'lock'" copy-content />
</div>
</template>

<script setup lang="ts">
import { useClipboard } from "@vueuse/core";
import type { CCtokenDataType } from '@/components/tokens/types.ts';

const props = defineProps<{
tokenData: CCtokenDataType;
}>();

const { isSupported, copy, copied } = useClipboard({
//@ts-ignore
content: props.content,
});

</script>

<style lang="scss" scoped>
#cc-card {
font-family: 'OCR A Extended';
background-image: url('@/assets/cc-background-AMEX.png');
background-repeat: no-repeat;
background-size: contain;
}
</style>
10 changes: 9 additions & 1 deletion frontend_vue/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ import {
faFileExcel,
faFile,
faQuoteLeft,
faArrowUpRightFromSquare
faArrowUpRightFromSquare,
faCreditCard,
faLock,
faCalendarDay,
faIdCard,
} from '@fortawesome/free-solid-svg-icons';
import { createVfm } from 'vue-final-modal';
import { vTooltip } from 'floating-vue';
Expand Down Expand Up @@ -70,6 +74,10 @@ library.add(
faFile,
faQuoteLeft,
faArrowUpRightFromSquare,
faCreditCard,
faLock,
faCalendarDay,
faIdCard,
);

const vfm = createVfm();
Expand Down
3 changes: 3 additions & 0 deletions frontend_vue/src/utils/formValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export const formValidators: ValidateSchemaType = {
[TOKENS_TYPE.DNS]: {
schema: Yup.object().shape(validationNotificationSettings),
},
[TOKENS_TYPE.CREDIT_CARD]: {
schema: Yup.object().shape(validationNotificationSettings),
},
[TOKENS_TYPE.QRCODE]: {
schema: Yup.object().shape(validationNotificationSettings),
},
Expand Down
13 changes: 13 additions & 0 deletions frontend_vue/src/utils/tokenServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ export const tokenServices: TokenServicesType = {
],
category: TOKEN_CATEGORY.OTHER,
},
[TOKENS_TYPE.CREDIT_CARD]: {
label: 'Credit Card',
description: 'Get an alert when an attacker attempt to use your credit card.',
documentationLink: 'https://docs.canarytokens.org/guide/qr-code-token.html',
icon: `${TOKENS_TYPE.CREDIT_CARD}.png`,
instruction: 'Place it on your computer and get notified when your\'e breached:',
howItWorksInstructions: [
'We give you unique Credit Card details',
'You place it somewhere.',
'We send you an alert when that Credit Card is used.',
],
category: TOKEN_CATEGORY.OTHER,
},
[TOKENS_TYPE.QRCODE]: {
label: 'QR code',
description: 'Get an alert when an attacker follows your QR Code.',
Expand Down