Skip to content

Commit

Permalink
feat: integrate demo mode
Browse files Browse the repository at this point in the history
  • Loading branch information
onmax committed Feb 26, 2025
1 parent 70972a7 commit adb1ab5
Show file tree
Hide file tree
Showing 7 changed files with 1,469 additions and 21 deletions.
4 changes: 4 additions & 0 deletions src/components/modals/BuyOptionsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from '@vue/composition-api';
import { PageBody, FiatAmount, CircleSpinner } from '@nimiq/vue-components';
import { useDemoStore } from '@/stores/Demo';
import Modal from './Modal.vue';
import CountrySelector from '../CountrySelector.vue';
import CountryFlag from '../CountryFlag.vue';
Expand Down Expand Up @@ -234,13 +235,16 @@ export default defineComponent({
const country = ref<Country>(null);
const { isDemoEnabled } = useDemoStore();
const isMoonpayAvailable = computed(() => { // eslint-disable-line arrow-body-style
if (isDemoEnabled.value) return true;
if (!config.moonpay.enabled) return false;
if (!country.value) return true;
return MOONPAY_COUNTRY_CODES.includes(country.value.code);
});
const isSimplexAvailable = computed(() => { // eslint-disable-line arrow-body-style
if (isDemoEnabled.value) return true;
if (!config.simplex.enabled) return false;
if (!country.value) return true;
return SIMPLEX_COUNTRY_CODES.includes(country.value.code);
Expand Down
146 changes: 146 additions & 0 deletions src/components/modals/demos/DemoModalBuy.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<template>
<Modal :showOverlay="showOverlay">
<PageHeader class="flex-column">
<h1 class="nq-h1">{{ $t('Buy NIM') }}</h1>
<div class="demo-warning nq-label">
{{ $t('DEMO') }}
</div>
</PageHeader>
<PageBody>
<div class="flex-row">

<AmountInput v-model="amount" :decimals="5">
<AmountMenu slot="suffix" class="ticker" currency="nim" :open="amountMenuOpened"
:activeCurrency="activeCurrency" :fiatCurrency="fiatCurrency" :feeOption="false"
:otherFiatCurrencies="otherFiatCurrencies"
@click.native.stop="amountMenuOpened = !amountMenuOpened"
/>
</AmountInput>
</div>
</PageBody>
<PageFooter>
<button class="nq-button light-blue" @click="buyDummyNim" :disabled="!amount">
{{ $t('Buy NIM') }}
</button>
</PageFooter>

<PageBody slot="overlay" class="overlay-content">
<HighFiveIcon />
<h2 class="nq-h2">
{{ $t('Your NIM is under its way!') }}
</h2>
<p>
{{ $t('This transaction is instant and secure.') }}
</p>
</PageBody>
</Modal>
</template>

<script lang="ts">
import { computed, defineComponent, ref } from '@vue/composition-api';
import { PageBody, PageHeader, PageFooter } from '@nimiq/vue-components';
import AmountInput from '@/components/AmountInput.vue';
import AmountMenu from '@/components/AmountMenu.vue';
import Modal from '@/components/modals/Modal.vue';
import { useAccountStore } from '@/stores/Account';
import { useFiatStore } from '@/stores/Fiat';
import { FIAT_CURRENCIES_OFFERED } from '@/lib/Constants';
// import { useTransactionsStore } from '@/stores/Transactions';
import { useDemoStore } from '@/stores/Demo';
import { useRouter } from '@/router';
import HighFiveIcon from '@/components/icons/HighFiveIcon.vue';
// import { useAddressStore } from '@/stores/Address';
export default defineComponent({
setup() {
const { activeCurrency } = useAccountStore();
const { currency: fiatCurrency } = useFiatStore();
const otherFiatCurrencies = computed(() =>
FIAT_CURRENCIES_OFFERED.filter((fiat) => fiat !== fiatCurrency.value));
const amount = ref(0);
const amountMenuOpened = ref(false);
const showOverlay = ref(false);
const router = useRouter();
// const { activeAddressInfo } = useAddressStore();
// const maxSendableAmount = computed(() => Math.max((activeAddressInfo.value!.balance || 0), 0));
// const sendMax = () => amount.value = maxSendableAmount.value;
function buyDummyNim() {
useDemoStore().buyDemoNim(amount.value);
showOverlay.value = true;
setTimeout(() => {
showOverlay.value = false;
router.push('/');
}, 4000);
}
return {
amount,
activeCurrency,
fiatCurrency,
otherFiatCurrencies,
amountMenuOpened,
buyDummyNim,
showOverlay,
// sendMax,
};
},
components: {
Modal,
AmountInput,
AmountMenu,
PageHeader,
PageBody,
PageFooter,
HighFiveIcon,
},
});
</script>

<style scoped lang="scss">
.small-page {
> .page-header {
overflow: hidden;
.demo-warning {
margin: 0;
text-align: center;
position: absolute;
top: 0;
left: 0;
right: 0;
background: var(--nimiq-orange-bg);
color: white;
padding: 0.5rem 0;
}
}
}
::v-deep .nq-card.overlay {
background: var(--nimiq-green);
color: white;
.overlay-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
svg {
width: 128px;
height: 128px;
}
p {
margin-top: 0;
text-wrap: pretty;
}
}
.close-button {
display: none;
}
}
</style>
35 changes: 35 additions & 0 deletions src/components/modals/demos/DemoModalFallback.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<template>
<Modal>
<PageHeader class="flex-column">
<h1 class="nq-h1">{{ $t('Nimiq Demo') }}</h1>
</PageHeader>
<PageBody>
<p class="nq-p">
{{ $t('This is not a real Nimiq Wallet. It is just a demo so it is limited in functionality.') }}
</p>
<p>
{{ $t('You can open a free NIM account in less than a minute.') }}
</p>
</PageBody>
<PageFooter>
<a href="https://wallet.nimiq.com" target="_blank" class="nq-button light-blue">
{{ $t('Open Nimiq Wallet') }}
</a>
</PageFooter>
</Modal>
</template>

<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { PageBody, PageHeader, PageFooter } from '@nimiq/vue-components';
import Modal from '../Modal.vue';
export default defineComponent({
components: {
Modal,
PageHeader,
PageBody,
PageFooter,
},
});
</script>
4 changes: 2 additions & 2 deletions src/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { WELCOME_MODAL_LOCALSTORAGE_KEY, WELCOME_STAKING_MODAL_LOCALSTORAGE_KEY
import { usePwaInstallPrompt } from './composables/usePwaInstallPrompt';
import type { SetupSwapWithKycResult, SWAP_KYC_HANDLER_STORAGE_KEY } from './swap-kyc-handler'; // avoid bundling
import type { RelayServerInfo } from './lib/usdc/OpenGSN';
import { HubApiMock, isPlaygroundEnabled } from './stores/Playground';
import { DemoHubApi, checkIfDemoIsActive } from './stores/Demo';

export function shouldUseRedirects(ignoreSettings = false): boolean {
if (!ignoreSettings) {
Expand Down Expand Up @@ -115,7 +115,7 @@ function getBehavior({

// We can't use the reactive config via useConfig() here because that one can only be used after the composition-api
// plugin has been registered in Vue 2.
const hubApi = new HubApi(Config.hubEndpoint);
const hubApi = checkIfDemoIsActive() ? DemoHubApi.create() : new HubApi(Config.hubEndpoint);

hubApi.on(HubApi.RequestType.ONBOARD, async (accounts) => {
const { config } = useConfig();
Expand Down
50 changes: 32 additions & 18 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { launchElectrum } from './electrum';
import { launchPolygon } from './ethers';
import { useAccountStore } from './stores/Account';
import { useFiatStore } from './stores/Fiat';
import { useDemoStore } from './stores/Demo';
import { useSettingsStore } from './stores/Settings';
import router from './router';
import { i18n, loadLanguage } from './i18n/i18n-setup';
Expand Down Expand Up @@ -48,14 +49,21 @@ Vue.use(VuePortal, { name: 'Portal' });

async function start() {
initPwa(); // Must be called as soon as possible to catch early browser events related to PWA
await initStorage(); // Must be awaited before starting Vue
initTrials(); // Must be called after storage was initialized, can affect Config
// Must run after VueCompositionApi has been enabled and after storage was initialized. Could potentially run in
// background and in parallel to syncFromHub, but RedirectRpcClient.init does not actually run async code anyways.
await initHubApi();
syncFromHub(); // Can run parallel to Vue initialization; must be called after storage was initialized.

serviceWorkerHasUpdate.then((hasUpdate) => useSettingsStore().state.updateAvailable = hasUpdate);
const { isDemoEnabled } = useDemoStore();

if (!isDemoEnabled.value) {
await initStorage(); // Must be awaited before starting Vue
initTrials(); // Must be called after storage was initialized, can affect Config
// Must run after VueCompositionApi has been enabled and after storage was initialized. Could potentially run in
// background and in parallel to syncFromHub, but RedirectRpcClient.init does not actually run async code
// anyways.
await initHubApi();
syncFromHub(); // Can run parallel to Vue initialization; must be called after storage was initialized.

serviceWorkerHasUpdate.then((hasUpdate) => useSettingsStore().state.updateAvailable = hasUpdate);
} else {
useDemoStore().initialize(router);
}

// Update exchange rates every 2 minutes or every 10 minutes, depending on whether the Wallet is currently actively
// used. If an update takes longer than that time due to a provider's rate limit, wait until the update succeeds
Expand Down Expand Up @@ -94,11 +102,15 @@ async function start() {
const { language } = useSettingsStore();
loadLanguage(language.value);

startSentry();
if (!isDemoEnabled.value) {
startSentry();
}

const { config } = useConfig();

if (config.environment !== ENV_MAIN) {
if (isDemoEnabled.value) {
document.title = 'Nimiq Wallet Demo';
} else if (config.environment !== ENV_MAIN) {
document.title = 'Nimiq Testnet Wallet';
}

Expand All @@ -107,15 +119,17 @@ async function start() {
initFastspotApi(config.fastspot.apiEndpoint, config.fastspot.apiKey);
});

watch(() => {
if (!config.oasis.apiEndpoint) return;
initOasisApi(config.oasis.apiEndpoint);
});
if (!isDemoEnabled.value) {
watch(() => {
if (!config.oasis.apiEndpoint) return;
initOasisApi(config.oasis.apiEndpoint);
});

watch(() => {
if (!config.ten31Pass.enabled) return;
initKycConnection();
});
watch(() => {
if (!config.ten31Pass.enabled) return;
initKycConnection();
});
}

// Make reactive config accessible in components
Vue.prototype.$config = config;
Expand Down
9 changes: 8 additions & 1 deletion src/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AddStakeEvent, ApiValidator, RawValidator, useStakingStore } from './st
import { ENV_MAIN, STAKING_CONTRACT_ADDRESS } from './lib/Constants';
import { reportToSentry } from './lib/Sentry';
import { useAccountStore } from './stores/Account';
import { useDemoStore } from './stores/Demo';

let isLaunched = false;
let clientPromise: Promise<Client>;
Expand Down Expand Up @@ -102,6 +103,7 @@ export async function launchNetwork() {
const transactionsStore = useTransactionsStore();
const addressStore = useAddressStore();
const stakingStore = useStakingStore();
const demoStore = useDemoStore();

const subscribedAddresses = new Set<string>();

Expand All @@ -118,7 +120,7 @@ export async function launchNetwork() {
network$.fetchingTxHistory--;

async function updateBalances(addresses: string[] = [...balances.keys()]) {
if (!addresses.length) return;
if (!addresses.length || demoStore.isDemoEnabled) return;
await client.waitForConsensusEstablished();
const accounts = await retry(() => client.getAccounts(addresses)).catch(reportFor('getAccounts'));
if (!accounts) return;
Expand Down Expand Up @@ -303,6 +305,7 @@ export async function launchNetwork() {
})();

function transactionListener(plain: PlainTransactionDetails) {
if (demoStore.isDemoEnabled) return;
if (plain.recipient === STAKING_CONTRACT_ADDRESS) {
if (plain.data.type === 'add-stake') {
if (!balances.has(plain.sender) && 'staker' in plain.data) {
Expand Down Expand Up @@ -340,6 +343,7 @@ export async function launchNetwork() {
}

function subscribe(addresses: string[]) {
if (demoStore.isDemoEnabled) return false;
client.addTransactionListener(transactionListener, addresses);
updateBalances(addresses);
updateStakes(addresses);
Expand All @@ -349,6 +353,7 @@ export async function launchNetwork() {
// Subscribe to new addresses (for balance updates and transactions)
// Also remove logged out addresses from fetched (so that they get fetched on next login)
watch(addressStore.addressInfos, () => {
if (demoStore.isDemoEnabled) return;
const newAddresses: string[] = [];
const removedAddresses = new Set(subscribedAddresses);

Expand Down Expand Up @@ -380,6 +385,7 @@ export async function launchNetwork() {

// Fetch transactions for active address
watch([addressStore.activeAddress, txFetchTrigger], ([activeAddress, trigger]) => {
if (demoStore.isDemoEnabled) return;
const address = activeAddress as string | null;
if (!address || fetchedAddresses.value.includes(address)) return;
addFetchedAddress(address);
Expand Down Expand Up @@ -428,6 +434,7 @@ export async function launchNetwork() {
// Fetch transactions for proxies
const proxyStore = useProxyStore();
watch(proxyStore.networkTrigger, () => {
if (demoStore.isDemoEnabled) return;
const newProxies: string[] = [];
const addressesToSubscribe: string[] = [];
for (const proxyAddress of proxyStore.allProxies.value) {
Expand Down
Loading

0 comments on commit adb1ab5

Please sign in to comment.