Skip to content

Commit

Permalink
frontend: integrate btc direct buy widget
Browse files Browse the repository at this point in the history
This is the first step of the BTC Direct integration and includes
the buy (fiat-to-coin) widget.

The widget requires linking to a CSS and loading a script from
BTC Direct as well as a few paramenters such as api-key.

To keep intergrations consistent and have a clean separation
between app and services, this commit includes a static page that
can be iframed, as BTC Direct does not provide an iframe solution.

Iframes have their own DOM and scoped CSS, so that our CSS cannot
accidentally mess up BTC Directs styles.

Currently data is only passed to the iframe via HTML attributes
but when needed we can use postMessage to communicate back to
the main app.

Disclaimers etc have just been copied from BTC Direct OTC info
and are currently placeholders, as the new disclaimers are not
yet final.
  • Loading branch information
thisconnect committed Jan 16, 2025
1 parent a3c724f commit 6118f57
Show file tree
Hide file tree
Showing 11 changed files with 419 additions and 81 deletions.
11 changes: 11 additions & 0 deletions frontends/web/public/btcdirect/btcdirect.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
html, body {
font-family: sans-serif;
height: 100%;
margin: 0;
padding: 0;
}

.btcdirect-widget {
display: flex;
justify-content: center;
}
117 changes: 117 additions & 0 deletions frontends/web/public/btcdirect/fiat-to-coin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BTC Direct - Buy</title>

<link href="./btcdirect.css" rel="stylesheet">

<div class="btcdirect-widget"></div>
<script src="./polyfill.js"></script>

<script lang="js">
;(() => {
const {
address,
apiKey,
baseCurrency,
mode,
quoteCurrency,
} = window.frameElement?.dataset;

if (mode === 'debug') {
console.info(window.frameElement?.dataset);
}

if ( // this should never happen, but if it does we stop here
!address
|| !baseCurrency
|| !quoteCurrency
) {
document.body.append(
Object.assign(document.createElement('h1'), {
style: 'color: red; padding: 1rem;',
textContent: `Unexpected error:
${!address ? 'Address missing' : ''}
${!baseCurrency ? 'BaseCurrency missing' : ''}
${!quoteCurrency ? 'QuoteCurrency missing' : ''}
`
})
);
return;
}

const currency = baseCurrency.toUpperCase();

// add the btcdirect CSS
document.head.appendChild(
Object.assign(document.createElement('link'), {
href: (
mode === 'production'
? 'https://cdn.btcdirect.eu/fiat-to-coin/fiat-to-coin.css'
: 'https://cdn-sandbox.btcdirect.eu/fiat-to-coin/fiat-to-coin.css'
),
rel: 'stylesheet',
})
);

// add the btcdirect script
(function (btc, d, i, r, e, c, t) {
btc[r] = btc[r] || function () {
(btc[r].q = btc[r].q || []).push(arguments)
};
c = d.createElement(i);
c.id = r; c.src = e; c.async = true;
c.type = 'module'; c.dataset.btcdirect = '';
t = d.getElementsByTagName(i)[0];
t.parentNode.insertBefore(c, t);
})(window, document, 'script', 'btcdirect',
mode === 'production'
? 'https://cdn.btcdirect.eu/fiat-to-coin/fiat-to-coin.js'
: 'https://cdn-sandbox.btcdirect.eu/fiat-to-coin/fiat-to-coin.js'
);

btcdirect('init', {
token: apiKey,
debug: mode === 'debug',
locale: window.frameElement?.dataset.locale || 'en-US',
theme: window.frameElement?.dataset.theme || 'light',
});

// fiat to coin order
btcdirect('wallet-addresses', {
addresses: {
address,
currency,
id: 'BitBox',
walletName: 'BitBox'
}
});
// Note that the wallet addresses that are provided affect the cryptocurrencies that can be selected.
// So if only one address for Bitcoin is provided, the only option to select in the “Choose a coin”-section will be Bitcoin.
// When providing addresses for multiple cryptocurrencies, the available cryptocurrencies will be selectable.
// When using the sandbox environment (https://cdn-sandbox.btcdirect.eu) all provided wallet addresses need to be testnet wallet addresses

btcdirect('set-parameters',
mode === 'production' ? {
baseCurrency: currency,
fixedCurrency: true,
quoteCurrency,
// paymentMethod: any of 'bancontact', 'bankTransfer', 'creditCard', 'giropay', 'iDeal', 'sofort', 'applepay'
showWalletAddress: false,
} : {
baseCurrency: currency,
fixedCurrency: true,
paymentMethod: 'sofort', // sandbox currently only supports sofort payment method
quoteCurrency,
showWalletAddress: false,
}
);

window.addEventListener('btcdirect-embeddable-fiat-to-coin-order-confirmed', (event) => {
consoel.log('btcdirect-embeddable-fiat-to-coin-order-confirmed', event);
// Handle the event
// Note that the sent information from the widget is found inside event.detail
});

})();
</script>
7 changes: 7 additions & 0 deletions frontends/web/public/btcdirect/polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
if (!crypto.randomUUID) {
crypto.randomUUID = function () {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
};
}
4 changes: 3 additions & 1 deletion frontends/web/src/api/exchanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ export type ExchangeDeal = {
isBest: boolean;
}

export type TExchangeName = 'moonpay' | 'pocket' | 'btcdirect';

export type ExchangeDeals = {
exchangeName: 'moonpay' | 'pocket' | 'btcdirect';
exchangeName: TExchangeName;
deals: ExchangeDeal[];
}

Expand Down
125 changes: 73 additions & 52 deletions frontends/web/src/components/terms/btcdirect-terms.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2024 Shift Crypto AG
* Copyright 2025 Shift Crypto AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,78 +14,99 @@
* limitations under the License.
*/

import { useTranslation } from 'react-i18next';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { isBitcoinOnly } from '@/routes/account/utils';
import { Button, Checkbox } from '@/components/forms';
import { setConfig } from '@/utils/config';
import { IAccount } from '@/api/account';
import { A } from '@/components/anchor/anchor';
import style from './terms.module.css';
import { i18n } from '@/i18n/i18n';
import { A } from '../anchor/anchor';

type TProps = {
onContinue: () => void;
account: IAccount;
onAgreedTerms: () => void;
}

export const BTCDirectTerms = ({ onContinue }: TProps) => {
export const BTCDirectTerms = ({ account, onAgreedTerms }: TProps) => {
const { t } = useTranslation();

const handleSkipDisclaimer = (e: ChangeEvent<HTMLInputElement>) => {
setConfig({ frontend: { skipBTCDirectDisclaimer: e.target.checked } });
};

const getPrivacyLink = () => {
switch (i18n.resolvedLanguage) {
case 'de':
return 'https://btcdirect.eu/de-at/datenschutzenklaerung?BitBox';
case 'nl':
return 'https://btcdirect.eu/nl-nl/privacy-policy?BitBox';
case 'es':
return 'https://btcdirect.eu/es-es/privacy-policy?BitBox';
case 'fr':
return 'https://btcdirect.eu/fr-fr/privacy-policy?BitBox';
default:
return 'https://btcdirect.eu/en-eu/privacy-policy?BitBox';
}
};
const coinCode = account.coinCode.toUpperCase();
const isBitcoin = isBitcoinOnly(account.coinCode);

// TODO: change the copy of MoonPay terms to BTCDirect
return (
<div className={style.disclaimerContainer}>
<div className={style.disclaimer}>
<h2 className={style.title}>{t('buy.exchange.infoContent.btcdirect.disclaimer.partnership.title')}</h2>
<p>{t('buy.exchange.infoContent.btcdirect.disclaimer.partnership.text')}</p>
<h2 className={style.title}>{t('buy.exchange.infoContent.btcdirect.disclaimer.personal.title')}</h2>
<p>{t('buy.exchange.infoContent.btcdirect.disclaimer.personal.text')}</p>
<h2 className={style.title}>{t('buy.exchange.infoContent.btcdirect.disclaimer.paymentMethods.title')}</h2>
<ul>
<li>
<p>
<strong>{t('buy.exchange.infoContent.btcdirect.disclaimer.paymentMethods.buy')}</strong>
&nbsp;
{t('buy.exchange.infoContent.btcdirect.disclaimer.paymentMethods.buy2')}
</p>
</li>
<li>
<p>
<strong>{t('buy.exchange.infoContent.btcdirect.disclaimer.paymentMethods.sell')}</strong>
&nbsp;
{t('buy.exchange.infoContent.btcdirect.disclaimer.paymentMethods.sell2')}
</p>
</li>
</ul>
<p>{t('buy.exchange.infoContent.btcdirect.disclaimer.paymentMethods.fee')}</p>
<h2 className={style.title}>{t('buy.exchange.infoContent.btcdirect.disclaimer.security.title')}</h2>
<p>{t('buy.exchange.infoContent.btcdirect.disclaimer.security.text')}</p>
BTC Direct
<h2 className={style.title}>
{t('buy.info.disclaimer.title', {
context: isBitcoin ? 'bitcoin' : 'crypto'
})}
</h2>
<p>{t('buy.info.disclaimer.intro.0', { coinCode })}</p>
<p>{t('buy.info.disclaimer.intro.1', { coinCode })}</p>
<h2 className={style.title}>
{t('buy.info.disclaimer.payment.title')}
</h2>
<p>{t('buy.info.disclaimer.payment.details', { coinCode })}</p>
<div className={style.table}>
<table>
<colgroup>
<col width="*" />
<col width="50px" />
<col width="*" />
</colgroup>
<thead>
<tr>
<th>{t('buy.info.disclaimer.payment.table.method')}</th>
<th>{t('buy.info.disclaimer.payment.table.fee')}</th>
<th>{t('buy.info.disclaimer.payment.table.description')}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{t('buy.info.disclaimer.payment.table.1_method')}</td>
<td className={style.nowrap}>1.9 %</td>
<td>{t('buy.info.disclaimer.payment.table.1_description')}</td>
</tr>
<tr>
<td>{t('buy.info.disclaimer.payment.table.2_method')}</td>
<td className={style.nowrap}>4.9 %</td>
<td>{t('buy.info.disclaimer.payment.table.2_description')}</td>
</tr>
</tbody>
</table>
</div>
<p>{t('buy.info.disclaimer.payment.footnote')}</p>
<h2 className={style.title}>
{t('buy.info.disclaimer.security.title')}
</h2>
<p>
{t('buy.info.disclaimer.security.descriptionGeneric', {
context: isBitcoin ? 'bitcoin' : 'crypto'
})}
</p>
<p>
<A href="https://bitbox.swiss/bitbox02/threat-model/">
{t('buy.exchange.infoContent.btcdirect.disclaimer.security.link')}
{t('buy.info.disclaimer.security.link')}
</A>
</p>
<h2 className={style.title}>{t('buy.exchange.infoContent.btcdirect.disclaimer.kyc.title')}</h2>
<p>{t('buy.exchange.infoContent.btcdirect.disclaimer.kyc.text')}</p>
<h2 className={style.title}>{t('buy.exchange.infoContent.btcdirect.disclaimer.dataProtection.title')}</h2>
<p>{t('buy.exchange.infoContent.btcdirect.disclaimer.dataProtection.text')}</p>
<h2 className={style.title}>
{t('buy.info.disclaimer.protection.title')}
</h2>
<p>
{t('buy.info.disclaimer.protection.descriptionGeneric', {
context: isBitcoin ? 'bitcoin' : 'crypto'
})}
</p>
<p>
<A href={getPrivacyLink()}>
{t('buy.exchange.infoContent.btcdirect.disclaimer.dataProtection.link')}
<A href="https://www.moonpay.com/privacy_policy">
{t('buy.info.disclaimer.privacyPolicy')}
</A>
</p>
</div>
Expand All @@ -98,7 +119,7 @@ export const BTCDirectTerms = ({ onContinue }: TProps) => {
<div className="buttons text-center m-bottom-xlarge">
<Button
primary
onClick={onContinue}>
onClick={onAgreedTerms}>
{t('buy.info.continue')}
</Button>
</div>
Expand Down
19 changes: 19 additions & 0 deletions frontends/web/src/i18n/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,22 @@ export const getRegionNameFromLocale = (nativeLocale: string): string => {
return '';
}
};

/**
* Workaround to add missing region info
* @param locale
* @returns locale-with-region
*/
export const getLocaleWithRegion = (locale: string) => {
if (locale.includes('-') || locale.includes('_')) {
return locale;
}
switch (locale) {
case 'en':
return 'en-US';
case 'de':
return 'de-DE';
default:
return `${locale}-${locale.toUpperCase()}`;
}
};
Loading

0 comments on commit 6118f57

Please sign in to comment.