feat(gui): Add a Introduction for new users (#287)
* feat(gui): add IntroductionModal component * feat(gui): add interactivity to IntroductionModal * feat(gui): create SlideTemplate component for IntroductionModal Slides * feat(gui): add generic slides to IntroductionModal with images and content * feat(gui): add Slide with SwapStatusAlert to IntroductionModal * feat(gui): show the introduction only on the first app start * feat(gui): make external links functional * fix(gui): update github link to link to active repo * feat(gui): replace old images with new mockups and update Slide05 content * feat(gui): add CardSelectionGroup and CardSelectionOption components for improved card selection UI * feat(gui): add FiatPricePreference slide to IntroductionModal * feat(gui): save user preference regarding fiat prices I set the initial store configuration for fetching fiat prices to false to avoid any calls to coingecko without user consent * refactor(gui): remove old Slide05 component for improved codebase maintenance * fix(gui): add UnstoppableSwap logo to FiatPricePreference slide * refactor(gui): update image imports and improve slide content for introduction modal * fix(gui): introduce ExternalLink component and update Slide05 to use it for external navigation * fix(gui): replace webp images for introduction with svg mockups for improved quality * fix(gui): change order of introduction slides, to asking for fiat price preference at the end * refactor(gui): implement CardSelectionContext for managing card selection state * refactor: texts in intro modakl * fix(gui): update currency fetching SVG for improved design and clarity * feat(gui): added changelog entry for introduction --------- Co-authored-by: Binarybaron <binarybaron@protonmail.com>
|
|
@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- GUI: Added introduction flow for first-time users
|
||||||
|
|
||||||
## [1.0.0-rc.19] - 2025-04-28
|
## [1.0.0-rc.19] - 2025-04-28
|
||||||
|
|
||||||
## [1.0.0-rc.18] - 2025-04-28
|
## [1.0.0-rc.18] - 2025-04-28
|
||||||
|
|
|
||||||
44
src-gui/src/assets/currencyFetching.svg
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
src-gui/src/assets/groupWithChatbubbles.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
73
src-gui/src/assets/mockConfigureSwap.svg
Normal file
|
After Width: | Height: | Size: 57 KiB |
51
src-gui/src/assets/mockHistoryPage.svg
Normal file
|
After Width: | Height: | Size: 46 KiB |
93
src-gui/src/assets/mockMakerSelection.svg
Normal file
|
After Width: | Height: | Size: 54 KiB |
75
src-gui/src/assets/simpleSwapFlowDiagram.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src-gui/src/assets/walletWithBitcoinAndMonero.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
|
|
@ -15,6 +15,7 @@ import { useEffect } from "react";
|
||||||
import { setupBackgroundTasks } from "renderer/background";
|
import { setupBackgroundTasks } from "renderer/background";
|
||||||
import "@fontsource/roboto";
|
import "@fontsource/roboto";
|
||||||
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
||||||
|
import IntroductionModal from "./modal/introduction/IntroductionModal";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
innerContent: {
|
innerContent: {
|
||||||
|
|
@ -36,6 +37,7 @@ export default function App() {
|
||||||
<ThemeProvider theme={themes[theme]}>
|
<ThemeProvider theme={themes[theme]}>
|
||||||
<GlobalSnackbarProvider>
|
<GlobalSnackbarProvider>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
<IntroductionModal/>
|
||||||
<Router>
|
<Router>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<InnerContent />
|
<InnerContent />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { createContext, useContext, useState, ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface CardSelectionContextType {
|
||||||
|
selectedValue: string
|
||||||
|
setSelectedValue: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardSelectionContext = createContext<CardSelectionContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
export function CardSelectionProvider({
|
||||||
|
children,
|
||||||
|
initialValue,
|
||||||
|
onChange
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
initialValue: string
|
||||||
|
onChange?: (value: string) => void
|
||||||
|
}) {
|
||||||
|
const [selectedValue, setSelectedValue] = useState(initialValue)
|
||||||
|
|
||||||
|
const handleValueChange = (value: string) => {
|
||||||
|
setSelectedValue(value)
|
||||||
|
onChange?.(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardSelectionContext.Provider value={{ selectedValue, setSelectedValue: handleValueChange }}>
|
||||||
|
{children}
|
||||||
|
</CardSelectionContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCardSelection() {
|
||||||
|
const context = useContext(CardSelectionContext)
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useCardSelection must be used within a CardSelectionProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Box } from '@material-ui/core'
|
||||||
|
import CheckIcon from '@material-ui/icons/Check'
|
||||||
|
import { CardSelectionProvider } from './CardSelectionContext'
|
||||||
|
|
||||||
|
interface CardSelectionGroupProps {
|
||||||
|
children: React.ReactElement<{ value: string }>[]
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CardSelectionGroup({
|
||||||
|
children,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: CardSelectionGroupProps) {
|
||||||
|
return (
|
||||||
|
<CardSelectionProvider initialValue={value} onChange={onChange}>
|
||||||
|
<Box style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 12 }}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</CardSelectionProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Box } from "@material-ui/core";
|
||||||
|
import CheckIcon from '@material-ui/icons/Check'
|
||||||
|
import { useCardSelection } from './CardSelectionContext'
|
||||||
|
|
||||||
|
// The value prop is used by the parent CardSelectionGroup to determine which option is selected
|
||||||
|
export default function CardSelectionOption({children, value}: {children: React.ReactNode, value: string}) {
|
||||||
|
const { selectedValue, setSelectedValue } = useCardSelection()
|
||||||
|
const selected = value === selectedValue
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
onClick={() => setSelectedValue(value)}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: 16,
|
||||||
|
border: selected ? '2px solid #FF5C1B' : '2px solid #555',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: '1em',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
border: selected ? '2px solid #FF5C1B' : '2px solid #555',
|
||||||
|
borderRadius: 99999,
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
background: selected ? '#FF5C1B' : 'transparent',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
transform: selected ? 'scale(1.1)' : 'scale(1)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selected ? (
|
||||||
|
<CheckIcon
|
||||||
|
style={{
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
transform: 'scale(1)',
|
||||||
|
animation: 'checkIn 0.2s ease-in-out'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
<Box pt={0.5}>{children}</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { makeStyles, Modal } from '@material-ui/core'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import Slide01_GettingStarted from './slides/Slide01_GettingStarted'
|
||||||
|
import Slide02_ChooseAMaker from './slides/Slide02_ChooseAMaker'
|
||||||
|
import Slide03_PrepareSwap from './slides/Slide03_PrepareSwap'
|
||||||
|
import Slide04_ExecuteSwap from './slides/Slide04_ExecuteSwap'
|
||||||
|
import Slide05_KeepAnEyeOnYourSwaps from './slides/Slide05_KeepAnEyeOnYourSwaps'
|
||||||
|
import Slide06_FiatPricePreference from './slides/Slide06_FiatPricePreference'
|
||||||
|
import Slide07_ReachOut from './slides/Slide07_ReachOut'
|
||||||
|
import {
|
||||||
|
setFetchFiatPrices,
|
||||||
|
setUserHasSeenIntroduction,
|
||||||
|
} from 'store/features/settingsSlice'
|
||||||
|
import { useAppDispatch, useSettings } from 'store/hooks'
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
modal: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
paper: {
|
||||||
|
width: '80%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function IntroductionModal() {
|
||||||
|
const userHasSeenIntroduction = useSettings(
|
||||||
|
(s) => s.userHasSeenIntroduction
|
||||||
|
)
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
// Handle Display State
|
||||||
|
const [open, setOpen] = useState<boolean>(!userHasSeenIntroduction)
|
||||||
|
const [showFiat, setShowFiat] = useState<boolean>(true)
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Slide Index
|
||||||
|
const [currentSlideIndex, setCurrentSlideIndex] = useState(0)
|
||||||
|
|
||||||
|
const handleContinue = () => {
|
||||||
|
if (currentSlideIndex == slideComponents.length - 1) {
|
||||||
|
handleClose()
|
||||||
|
dispatch(setUserHasSeenIntroduction(true))
|
||||||
|
dispatch(setFetchFiatPrices(showFiat))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentSlideIndex((i) => i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePrevious = () => {
|
||||||
|
if (currentSlideIndex == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentSlideIndex((i) => i - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const slideComponents = [
|
||||||
|
<Slide01_GettingStarted
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
hidePreviousButton
|
||||||
|
/>,
|
||||||
|
<Slide02_ChooseAMaker
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
/>,
|
||||||
|
<Slide03_PrepareSwap
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
/>,
|
||||||
|
<Slide04_ExecuteSwap
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
/>,
|
||||||
|
<Slide05_KeepAnEyeOnYourSwaps
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
/>,
|
||||||
|
<Slide06_FiatPricePreference
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
showFiat={showFiat}
|
||||||
|
onChange={(showFiatSetting: string) =>
|
||||||
|
setShowFiat(showFiatSetting === 'show')
|
||||||
|
}
|
||||||
|
/>,
|
||||||
|
<Slide07_ReachOut
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
/>,
|
||||||
|
]
|
||||||
|
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
className={classes.modal}
|
||||||
|
disableAutoFocus
|
||||||
|
closeAfterTransition
|
||||||
|
>
|
||||||
|
{slideComponents[currentSlideIndex]}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Typography } from '@material-ui/core'
|
||||||
|
import SlideTemplate from './SlideTemplate'
|
||||||
|
import imagePath from 'assets/walletWithBitcoinAndMonero.png'
|
||||||
|
|
||||||
|
export default function Slide01_GettingStarted(props: slideProps) {
|
||||||
|
return (
|
||||||
|
<SlideTemplate
|
||||||
|
title="Getting Started"
|
||||||
|
{...props}
|
||||||
|
imagePath={imagePath}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle1">
|
||||||
|
To start swapping, you'll need:
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
<ul>
|
||||||
|
<li>A Bitcoin wallet with funds to swap</li>
|
||||||
|
<li>A Monero wallet to receive your Monero</li>
|
||||||
|
</ul>
|
||||||
|
</Typography>
|
||||||
|
</SlideTemplate>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Typography } from '@material-ui/core'
|
||||||
|
import SlideTemplate from './SlideTemplate'
|
||||||
|
import imagePath from 'assets/mockMakerSelection.svg'
|
||||||
|
|
||||||
|
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||||
|
return (
|
||||||
|
<SlideTemplate
|
||||||
|
title="Choose a Maker"
|
||||||
|
stepLabel="Step 1"
|
||||||
|
{...props}
|
||||||
|
imagePath={imagePath}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle1">
|
||||||
|
To start a swap, choose a maker. Each maker offers different exchange rates and limits.
|
||||||
|
</Typography>
|
||||||
|
</SlideTemplate>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Typography } from '@material-ui/core'
|
||||||
|
import SlideTemplate from './SlideTemplate'
|
||||||
|
import imagePath from 'assets/mockConfigureSwap.svg'
|
||||||
|
|
||||||
|
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||||
|
return (
|
||||||
|
<SlideTemplate title="Prepare Swap" stepLabel="Step 2" {...props} imagePath={imagePath}>
|
||||||
|
<Typography variant="subtitle1">
|
||||||
|
To initiate a swap, provide a Monero address and optionally a Bitcoin refund address.
|
||||||
|
</Typography>
|
||||||
|
</SlideTemplate>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Typography } from '@material-ui/core'
|
||||||
|
import SlideTemplate from './SlideTemplate'
|
||||||
|
import imagePath from 'assets/simpleSwapFlowDiagram.svg'
|
||||||
|
|
||||||
|
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||||
|
return (
|
||||||
|
<SlideTemplate
|
||||||
|
title="Execute Swap"
|
||||||
|
stepLabel="Step 3"
|
||||||
|
{...props}
|
||||||
|
imagePath={imagePath}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle1">
|
||||||
|
After confirming:
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
<ol>
|
||||||
|
<li>Your Bitcoin are locked</li>
|
||||||
|
<li>Maker locks the Monero</li>
|
||||||
|
<li>Maker reedems the Bitcoin</li>
|
||||||
|
<li>Monero is sent to your address</li>
|
||||||
|
</ol>
|
||||||
|
</Typography>
|
||||||
|
</SlideTemplate>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Link, Typography } from '@material-ui/core'
|
||||||
|
import SlideTemplate from './SlideTemplate'
|
||||||
|
import imagePath from 'assets/mockHistoryPage.svg'
|
||||||
|
import ExternalLink from 'renderer/components/other/ExternalLink'
|
||||||
|
|
||||||
|
export default function Slide05_KeepAnEyeOnYourSwaps(props: slideProps) {
|
||||||
|
return (
|
||||||
|
<SlideTemplate
|
||||||
|
title="Monitor Your Swaps"
|
||||||
|
stepLabel="Step 3"
|
||||||
|
{...props}
|
||||||
|
imagePath={imagePath}
|
||||||
|
>
|
||||||
|
<Typography>
|
||||||
|
Monitor active swaps to ensure everything proceeds smoothly.
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
<ExternalLink href='https://docs.unstoppableswap.net/usage/first_swap'>
|
||||||
|
Learn more about atomic swaps
|
||||||
|
</ExternalLink>
|
||||||
|
</Typography>
|
||||||
|
</SlideTemplate>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Box, Typography, Paper, Button, Slide } from '@material-ui/core'
|
||||||
|
import CardSelectionGroup from 'renderer/components/inputs/CardSelection/CardSelectionGroup'
|
||||||
|
import CardSelectionOption from 'renderer/components/inputs/CardSelection/CardSelectionOption'
|
||||||
|
import SlideTemplate from './SlideTemplate'
|
||||||
|
import imagePath from 'assets/currencyFetching.svg'
|
||||||
|
|
||||||
|
const FiatPricePreferenceSlide = ({
|
||||||
|
handleContinue,
|
||||||
|
handlePrevious,
|
||||||
|
showFiat,
|
||||||
|
onChange,
|
||||||
|
}: slideProps & {
|
||||||
|
showFiat: boolean
|
||||||
|
onChange: (value: string) => void
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<SlideTemplate handleContinue={handleContinue} handlePrevious={handlePrevious} title="Fiat Prices" imagePath={imagePath}>
|
||||||
|
<Typography variant="subtitle1" color="textSecondary">
|
||||||
|
Do you want to show fiat prices?
|
||||||
|
</Typography>
|
||||||
|
<CardSelectionGroup
|
||||||
|
value={showFiat ? 'show' : 'hide'}
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
<CardSelectionOption value="show">
|
||||||
|
<Typography>Show fiat prices</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="textSecondary"
|
||||||
|
paragraph
|
||||||
|
style={{ marginBottom: 4 }}
|
||||||
|
>
|
||||||
|
We connect to CoinGecko to provide realtime currency
|
||||||
|
prices.
|
||||||
|
</Typography>
|
||||||
|
</CardSelectionOption>
|
||||||
|
<CardSelectionOption value="hide">
|
||||||
|
<Typography>Don't show fiat prices</Typography>
|
||||||
|
</CardSelectionOption>
|
||||||
|
</CardSelectionGroup>
|
||||||
|
<Box style={{ marginTop: "0.5rem" }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="textSecondary"
|
||||||
|
>
|
||||||
|
You can change your preference later in the settings
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</SlideTemplate>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FiatPricePreferenceSlide
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Box, Typography } from '@material-ui/core'
|
||||||
|
import SlideTemplate from './SlideTemplate'
|
||||||
|
import imagePath from 'assets/groupWithChatbubbles.png'
|
||||||
|
import GitHubIcon from "@material-ui/icons/GitHub"
|
||||||
|
import MatrixIcon from 'renderer/components/icons/MatrixIcon'
|
||||||
|
import LinkIconButton from 'renderer/components/icons/LinkIconButton'
|
||||||
|
|
||||||
|
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||||
|
return (
|
||||||
|
<SlideTemplate title="Reach out" {...props} imagePath={imagePath} customContinueButtonText="Get Started">
|
||||||
|
<Typography variant="subtitle1">
|
||||||
|
We would love to hear about your experience with Unstoppable
|
||||||
|
Swap and invite you to join our community.
|
||||||
|
</Typography>
|
||||||
|
<Box mt={3}>
|
||||||
|
<LinkIconButton url="https://github.com/UnstoppableSwap/core">
|
||||||
|
<GitHubIcon/>
|
||||||
|
</LinkIconButton>
|
||||||
|
<LinkIconButton url="https://matrix.to/#/#unstoppableswap:matrix.org">
|
||||||
|
<MatrixIcon/>
|
||||||
|
</LinkIconButton>
|
||||||
|
</Box>
|
||||||
|
</SlideTemplate>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { makeStyles, Paper, Box, Typography, Button } from '@material-ui/core'
|
||||||
|
|
||||||
|
type slideTemplateProps = {
|
||||||
|
handleContinue: () => void
|
||||||
|
handlePrevious: () => void
|
||||||
|
hidePreviousButton?: boolean
|
||||||
|
stepLabel?: String
|
||||||
|
title: String
|
||||||
|
children?: React.ReactNode
|
||||||
|
imagePath?: string
|
||||||
|
imagePadded?: boolean
|
||||||
|
customContinueButtonText?: String
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
paper: {
|
||||||
|
height: "80%",
|
||||||
|
width: "80%",
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
stepLabel: {
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
splitImage: {
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
objectFit: 'contain'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function SlideTemplate({
|
||||||
|
handleContinue,
|
||||||
|
handlePrevious,
|
||||||
|
hidePreviousButton,
|
||||||
|
stepLabel,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
imagePath,
|
||||||
|
imagePadded,
|
||||||
|
customContinueButtonText
|
||||||
|
}: slideTemplateProps) {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className={classes.paper}>
|
||||||
|
<Box m={3} flex alignContent="center" position="relative" width="50%" flexGrow={1}>
|
||||||
|
<Box>
|
||||||
|
{stepLabel && (
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
className={classes.stepLabel}
|
||||||
|
>
|
||||||
|
{stepLabel}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Typography variant="h4" style={{ marginBottom: 16 }}>{title}</Typography>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
bottom={0}
|
||||||
|
width="100%"
|
||||||
|
display="flex"
|
||||||
|
justifyContent={
|
||||||
|
hidePreviousButton ? 'flex-end' : 'space-between'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!hidePreviousButton && (
|
||||||
|
<Button onClick={handlePrevious}>Back</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={handleContinue}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{customContinueButtonText ? customContinueButtonText : 'Next' }
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{imagePath && (
|
||||||
|
<Box
|
||||||
|
bgcolor="#212121"
|
||||||
|
width="50%"
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
p={imagePadded ? "1.5em" : 0}
|
||||||
|
>
|
||||||
|
<img src={imagePath} className={classes.splitImage} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
type slideProps = {
|
||||||
|
handleContinue: () => void
|
||||||
|
handlePrevious: () => void
|
||||||
|
hidePreviousButton?: boolean
|
||||||
|
}
|
||||||
|
|
@ -35,7 +35,7 @@ export default function NavigationFooter() {
|
||||||
<Box className={classes.linksOuter}>
|
<Box className={classes.linksOuter}>
|
||||||
<Tooltip title="Check out the GitHub repository">
|
<Tooltip title="Check out the GitHub repository">
|
||||||
<span>
|
<span>
|
||||||
<LinkIconButton url="https://github.com/UnstoppableSwap/unstoppableswap-gui">
|
<LinkIconButton url="https://github.com/UnstoppableSwap/core">
|
||||||
<GitHubIcon />
|
<GitHubIcon />
|
||||||
</LinkIconButton>
|
</LinkIconButton>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
10
src-gui/src/renderer/components/other/ExternalLink.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Link from "@material-ui/core/Link";
|
||||||
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
|
|
||||||
|
export default function ExternalLink({children, href}: {children: React.ReactNode, href: string}) {
|
||||||
|
return (
|
||||||
|
<Link style={{cursor: 'pointer'}} onClick={() => open(href)}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ export interface SettingsState {
|
||||||
fiatCurrency: FiatCurrency;
|
fiatCurrency: FiatCurrency;
|
||||||
/// Whether to enable Tor for p2p connections
|
/// Whether to enable Tor for p2p connections
|
||||||
enableTor: boolean
|
enableTor: boolean
|
||||||
|
userHasSeenIntroduction: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FiatCurrency {
|
export enum FiatCurrency {
|
||||||
|
|
@ -98,9 +99,10 @@ const initialState: SettingsState = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
theme: Theme.Darker,
|
theme: Theme.Darker,
|
||||||
fetchFiatPrices: true,
|
fetchFiatPrices: false,
|
||||||
fiatCurrency: FiatCurrency.Usd,
|
fiatCurrency: FiatCurrency.Usd,
|
||||||
enableTor: true
|
enableTor: true,
|
||||||
|
userHasSeenIntroduction: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const alertsSlice = createSlice({
|
const alertsSlice = createSlice({
|
||||||
|
|
@ -135,6 +137,9 @@ const alertsSlice = createSlice({
|
||||||
removeNode(slice, action: PayloadAction<{ network: Network, type: Blockchain, node: string }>) {
|
removeNode(slice, action: PayloadAction<{ network: Network, type: Blockchain, node: string }>) {
|
||||||
slice.nodes[action.payload.network][action.payload.type] = slice.nodes[action.payload.network][action.payload.type].filter(node => node !== action.payload.node);
|
slice.nodes[action.payload.network][action.payload.type] = slice.nodes[action.payload.network][action.payload.type].filter(node => node !== action.payload.node);
|
||||||
},
|
},
|
||||||
|
setUserHasSeenIntroduction(slice, action: PayloadAction<boolean>) {
|
||||||
|
slice.userHasSeenIntroduction = action.payload
|
||||||
|
},
|
||||||
resetSettings(_) {
|
resetSettings(_) {
|
||||||
return initialState;
|
return initialState;
|
||||||
},
|
},
|
||||||
|
|
@ -153,6 +158,7 @@ export const {
|
||||||
setFetchFiatPrices,
|
setFetchFiatPrices,
|
||||||
setFiatCurrency,
|
setFiatCurrency,
|
||||||
setTorEnabled,
|
setTorEnabled,
|
||||||
|
setUserHasSeenIntroduction,
|
||||||
} = alertsSlice.actions;
|
} = alertsSlice.actions;
|
||||||
|
|
||||||
export default alertsSlice.reducer;
|
export default alertsSlice.reducer;
|
||||||
|
|
|
||||||