-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[MIK-323] Add export for other devices button. (#158)
* add export to devices modal in slice * install que generator and add logo image * create and use deviceExport modal with qr generation * add encrypt library and make a function for encrypt the json * add backend post * add user id into interactor * add missing props in tests * Fix qr upload * Add botId and redirect clipboard to parent window * change button style and add time left to expire * hot fix * fix encryption function * Fix time left --------- Co-authored-by: Mikudev <[email protected]>
- Loading branch information
1 parent
bfac775
commit 8f109a8
Showing
16 changed files
with
343 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
@import '../../variables'; | ||
|
||
.deviceExport { | ||
&__button { | ||
color: $color-white; | ||
padding: 0 4px; | ||
border-radius: 10px; | ||
transition: ease-in-out 0.3s; | ||
|
||
&:hover { | ||
color: $secondary-color; | ||
} | ||
} | ||
|
||
&__loading { | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
width: 100%; | ||
height: 100%; | ||
} | ||
|
||
&__container { | ||
display: flex; | ||
align-items: center; | ||
flex-direction: column; | ||
position: relative; | ||
width: 100%; | ||
|
||
&__close { | ||
position: absolute; | ||
top: 0; | ||
right: 0; | ||
cursor: pointer; | ||
|
||
&:hover { | ||
color: $color-red; | ||
} | ||
} | ||
|
||
&__header { | ||
margin-bottom: 10px; | ||
|
||
& p { | ||
font-size: 0.8rem; | ||
font-style: italic; | ||
margin-top: 6px; | ||
color: $color-gray; | ||
font-weight: 500; | ||
} | ||
} | ||
|
||
&__hash { | ||
width: 100%; | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: center; | ||
border: 1px solid $color-gray; | ||
padding: 4px 6px; | ||
gap: 8px; | ||
border-radius: 8px; | ||
margin-top: 10px; | ||
|
||
& p { | ||
width: 100%; | ||
text-wrap: nowrap; | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
} | ||
|
||
& button { | ||
color: $color-gray; | ||
|
||
&:hover { | ||
color: white; | ||
} | ||
} | ||
} | ||
|
||
&__expiration { | ||
font-size: 0.8rem; | ||
margin-top: 4px; | ||
color: $color-gray; | ||
font-style: italic; | ||
} | ||
} | ||
|
||
&__modal { | ||
max-width: 400px !important; | ||
} | ||
} | ||
.disabled-export { | ||
color: $color-gray; | ||
cursor: not-allowed; | ||
&:hover { | ||
color: $color-gray; | ||
} | ||
} |
167 changes: 167 additions & 0 deletions
167
apps/interactor/src/components/history/DeviceExport.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import { Loader, Modal, Tooltip } from '@mikugg/ui-kit'; | ||
import CryptoJS from 'crypto-js'; | ||
import { QRCodeCanvas } from 'qrcode.react'; | ||
import React, { useEffect, useState } from 'react'; | ||
import { IoQrCode } from 'react-icons/io5'; | ||
|
||
import { FaCheck, FaClipboard } from 'react-icons/fa'; | ||
import { v4 as randomUUID } from 'uuid'; | ||
import { useAppContext } from '../../App.context'; | ||
import { setDeviceExportModal } from '../../state/slices/settingsSlice'; | ||
import { useAppDispatch, useAppSelector } from '../../state/store'; | ||
|
||
import { IoIosCloseCircleOutline } from 'react-icons/io'; | ||
import { toast } from 'react-toastify'; | ||
import './DeviceExport.scss'; | ||
import { uploadNarration } from '../../libs/platformAPI'; | ||
import { CustomEventType, postMessage } from '../../libs/stateEvents'; | ||
|
||
function stringToBase64(str: string): string { | ||
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(str)); | ||
} | ||
|
||
interface QRProps { | ||
value: string | null; | ||
expirationDate: number; | ||
loading: boolean; | ||
copied: boolean; | ||
} | ||
|
||
const intialQRState: QRProps = { | ||
value: null, | ||
expirationDate: Date.now(), | ||
loading: false, | ||
copied: false, | ||
}; | ||
|
||
export const DeviceExport = (): React.ReactNode => { | ||
const dispatch = useAppDispatch(); | ||
const [QR, setQR] = useState<QRProps>(intialQRState); | ||
const state = useAppSelector((state) => state); | ||
const { isPremium } = useAppSelector((state) => state.settings.user); | ||
const isModalOpen = useAppSelector((state) => state.settings.modals.deviceExport); | ||
const { isProduction, apiEndpoint, botId } = useAppContext(); | ||
const [_, forceUpdate] = useState(0); | ||
|
||
const getEncryptedJson = (): { | ||
encryptionKey: string; | ||
encryptedData: string; | ||
} => { | ||
const clonedState = JSON.parse(JSON.stringify(state)); | ||
clonedState.settings.modals.history = false; | ||
clonedState.botId = botId; | ||
const json = JSON.stringify(clonedState); | ||
const encryptionKey = randomUUID(); | ||
const encryptedData: string = CryptoJS.AES.encrypt(json, encryptionKey).toString(); | ||
return { encryptionKey, encryptedData }; | ||
}; | ||
|
||
const handleExport = async () => { | ||
dispatch(setDeviceExportModal(true)); | ||
setQR((qr) => ({ ...qr, loading: true })); | ||
|
||
try { | ||
const { encryptedData, encryptionKey } = getEncryptedJson(); | ||
const uploadResult = await uploadNarration(apiEndpoint, encryptedData); | ||
const qrValue = uploadResult?.filename ? stringToBase64(`${uploadResult.filename}#${encryptionKey}`) : null; | ||
console.log('uploadResult?.expiration', uploadResult?.expiration); | ||
console.log('Date.now()', Date.now()); | ||
setQR({ | ||
loading: false, | ||
value: qrValue, | ||
copied: false, | ||
expirationDate: uploadResult?.expiration ? new Date(uploadResult.expiration).getTime() : Date.now(), | ||
}); | ||
} catch (error) { | ||
setQR({ ...QR, loading: false }); | ||
dispatch(setDeviceExportModal(false)); | ||
toast.error('Error encrypting narration'); | ||
} | ||
}; | ||
|
||
const handleCopyHash = () => { | ||
postMessage(CustomEventType.COPY_TO_CLIPBOARD, QR.value || ''); | ||
setQR((qr) => ({ ...qr, copied: true })); | ||
toast.success('Key copied to clipboard'); | ||
setTimeout(() => { | ||
setQR((qr) => ({ ...qr, copied: false })); | ||
}, 4000); | ||
}; | ||
|
||
const handleCloseModal = () => { | ||
if (QR.loading) return undefined; | ||
setQR(intialQRState); | ||
dispatch(setDeviceExportModal(false)); | ||
}; | ||
const timeLeft = Math.floor((QR.expirationDate - Date.now()) / 1000 / 60); | ||
|
||
useEffect(() => { | ||
setInterval(() => { | ||
forceUpdate((prev) => prev + 1); | ||
}, 60000); | ||
}, []); | ||
|
||
if (!isProduction) return null; | ||
return ( | ||
<> | ||
<button | ||
data-tooltip-id="device-export-tooltip" | ||
data-tooltip-content={ | ||
isPremium | ||
? 'Export this narration to other device.' | ||
: 'Export to other devices is only available for premium members.' | ||
} | ||
className={`deviceExport__button ${!isPremium ? 'disabled-export' : ''}`} | ||
onClick={handleExport} | ||
disabled={!isPremium} | ||
> | ||
<IoQrCode /> | ||
</button> | ||
<Tooltip id="device-export-tooltip" place="bottom" /> | ||
<Modal className="deviceExport__modal" opened={isModalOpen} onCloseModal={handleCloseModal}> | ||
{QR.loading ? ( | ||
<div className="deviceExport__loading"> | ||
<Loader /> | ||
<p>Encrypting narration as QR...</p> | ||
</div> | ||
) : ( | ||
<div className="deviceExport__container"> | ||
<IoIosCloseCircleOutline onClick={handleCloseModal} size={20} className="deviceExport__container__close" /> | ||
<div className="deviceExport__container__header"> | ||
<h2>Export narration</h2> | ||
<p>Scan the QR code or copy the key to import this narration to another device.</p> | ||
</div> | ||
<div className="deviceExport__container__code"> | ||
{/* eslint-disable-next-line */} | ||
{/* @ts-ignore */} | ||
<QRCodeCanvas | ||
size={256} | ||
bgColor="transparent" | ||
fgColor="#ffffff" | ||
value={QR.value || ''} | ||
imageSettings={{ | ||
src: '../../../public/images/logo.png', | ||
x: undefined, | ||
y: undefined, | ||
height: 50, | ||
width: 50, | ||
opacity: 1, | ||
excavate: true, | ||
}} | ||
/> | ||
</div> | ||
<div className="deviceExport__container__hash"> | ||
<p>{QR.value}</p> | ||
<button disabled={QR.copied} onClick={handleCopyHash}> | ||
{QR.copied ? <FaCheck color="#00ff33" /> : <FaClipboard />} | ||
</button> | ||
</div> | ||
<div className="deviceExport__container__expiration"> | ||
{timeLeft > 0 ? `Expires in ${timeLeft} minutes` : 'Expired'} | ||
</div> | ||
</div> | ||
)} | ||
</Modal> | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.