Skip to content

Commit

Permalink
Rough cut of notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
rm-hull committed May 30, 2022
1 parent 499f083 commit 945ea90
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 50 deletions.
61 changes: 13 additions & 48 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,67 +12,27 @@ import {
Wrap,
WrapItem,
} from "@chakra-ui/react";
import base32Encode from "base32-encode";
import * as OTPAuth from "otpauth";
import QRCode from "qrcode.react";
import React from "react";
import { FiCheck, FiClipboard } from "react-icons/fi";
import { FiActivity, FiCheck, FiClipboard } from "react-icons/fi";
import { getFavicon } from "../favicons";
import { MigrationPayload } from "../proto/migration_payload";
import { getEncodedSecret, getTotp } from "../otp";
import { OTP } from "../types";
import HashTag from "./HashTag";

type CardProps = {
otp: OTP;
refresh?: number;
showQRCode?: boolean;
enableNotifications?: boolean;
onNotify?: () => void;
};

const getAlgorithm = (alg?: MigrationPayload.Algorithm) => {
switch (alg) {
case MigrationPayload.Algorithm.ALGORITHM_MD5:
return "MD5";
case MigrationPayload.Algorithm.ALGORITHM_SHA1:
return "SHA1";
case MigrationPayload.Algorithm.ALGORITHM_SHA256:
return "SHA256";
case MigrationPayload.Algorithm.ALGORITHM_SHA512:
return "SHA512";
default:
return undefined;
}
};

const getDigits = (digits?: MigrationPayload.DigitCount) => {
switch (digits) {
case MigrationPayload.DigitCount.DIGIT_COUNT_SIX:
return 6;
case MigrationPayload.DigitCount.DIGIT_COUNT_EIGHT:
return 8;
default:
return undefined;
}
};

const Card = React.memo(({ otp, showQRCode }: CardProps): JSX.Element => {
const encodedSecret = React.useMemo(
() => otp.secret && base32Encode(Uint8Array.from(Object.values(otp.secret)), "RFC4648"),
[otp.secret]
);
const totp = React.useMemo(
() =>
new OTPAuth.TOTP({
issuer: otp.issuer,
label: otp.name,
algorithm: getAlgorithm(otp.algorithm),
digits: getDigits(otp.digits),
period: 30,
secret: encodedSecret,
}),
[otp, encodedSecret]
);
const Card = React.memo(({ otp, showQRCode, enableNotifications, onNotify }: CardProps): JSX.Element => {
const encodedSecret = React.useMemo(() => getEncodedSecret(otp), [otp]);
const totp = React.useMemo(() => getTotp(otp, encodedSecret), [otp, encodedSecret]);

const code = totp.generate();
const code = totp!.generate();
const { hasCopied, onCopy } = useClipboard(code);

const bg = useColorModeValue("white", "var(--chakra-colors-gray-900)");
Expand Down Expand Up @@ -110,6 +70,11 @@ const Card = React.memo(({ otp, showQRCode }: CardProps): JSX.Element => {
)}

<Stack align="center" justify="center" direction="row" mt={showQRCode ? 4 : 0}>
{enableNotifications && (
<Tooltip label="Notify">
<IconButton aria-label="Notify" onClick={onNotify} icon={<FiActivity />} />
</Tooltip>
)}
<Heading fontSize="5xl" fontFamily="body">
{code}
</Heading>
Expand Down
31 changes: 29 additions & 2 deletions src/components/Group.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Box, SimpleGrid } from "@chakra-ui/react";
import format from "format-duration";
import hash from "object-hash";
import React, { useMemo, useState } from "react";
import { useHarmonicIntervalFn } from "react-use";
import { getFavicon } from "../favicons";
import useGeneralSettings from "../hooks/useGeneralSettings";
import useOtpParameters from "../hooks/useOtpParameters";
import { sort } from "../otp";
import { getEncodedSecret, getTotp, sort } from "../otp";
import { OTP } from "../types";
import Card from "./Card";

Expand All @@ -16,12 +18,30 @@ export default function Group({ filter = () => true }: GroupProps): JSX.Element
const { data } = useOtpParameters();
const [settings] = useGeneralSettings();
const [refresh, setRefresh] = useState<number | undefined>(undefined);
const [otp, setOtp] = useState<OTP | undefined>(undefined);
const encodedSecret = React.useMemo(() => getEncodedSecret(otp), [otp]);
const totp = React.useMemo(() => getTotp(otp, encodedSecret), [otp, encodedSecret]);

useHarmonicIntervalFn(() => {
const now = Date.now();
const seconds = Math.floor(now / 1000) % 60;
const timeLeft = 29 - (seconds % 30);
const overdue = now - (refresh ?? 0) > 40000;
setRefresh(timeLeft === 0 || overdue ? now : undefined);

if (settings?.enableNotifications && !!otp) {
new Notification(`${otp.issuer}: ${otp.name}`, {
body: `${totp?.generate()} (${format(timeLeft * 1000)})`,
tag: `zaup2`,
icon: getFavicon(otp),
requireInteraction: true,
});
// notification.onclose = (event) => {
// console.log("onclose called", { event });
// setOtp(undefined);
// notification.close();
// };
}
}, 1000);

const filtered = useMemo(() => sort(data)?.filter(filter), [data, filter]);
Expand All @@ -30,7 +50,14 @@ export default function Group({ filter = () => true }: GroupProps): JSX.Element
<Box textAlign="center" fontSize="xl">
<SimpleGrid minChildWidth="320px" spacing="10px" alignItems="start">
{filtered.map((otp) => (
<Card key={hash(otp)} otp={otp} refresh={refresh} showQRCode={settings?.showQRCode} />
<Card
key={hash(otp)}
otp={otp}
refresh={refresh}
showQRCode={settings?.showQRCode}
enableNotifications={settings?.enableNotifications}
onNotify={() => setOtp(otp)}
/>
))}
</SimpleGrid>
</Box>
Expand Down
44 changes: 44 additions & 0 deletions src/otp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import base32Encode from "base32-encode";
import * as OTPAuth from "otpauth";
import * as R from "ramda";

import { MigrationPayload } from "./proto/migration_payload";
import { OTP } from "./types";

export const normalize = (otp: OTP): OTP => {
Expand All @@ -12,3 +16,43 @@ export const normalize = (otp: OTP): OTP => {
};

export const sort = R.sortBy<OTP>((otp) => (otp.label ?? otp.issuer ?? "Unknown").toLowerCase());

export const getAlgorithm = (alg?: MigrationPayload.Algorithm) => {
switch (alg) {
case MigrationPayload.Algorithm.ALGORITHM_MD5:
return "MD5";
case MigrationPayload.Algorithm.ALGORITHM_SHA1:
return "SHA1";
case MigrationPayload.Algorithm.ALGORITHM_SHA256:
return "SHA256";
case MigrationPayload.Algorithm.ALGORITHM_SHA512:
return "SHA512";
default:
return undefined;
}
};

export const getDigits = (digits?: MigrationPayload.DigitCount) => {
switch (digits) {
case MigrationPayload.DigitCount.DIGIT_COUNT_SIX:
return 6;
case MigrationPayload.DigitCount.DIGIT_COUNT_EIGHT:
return 8;
default:
return undefined;
}
};

export const getTotp = (otp?: OTP, encodedSecret?: string): OTPAuth.TOTP | undefined =>
otp &&
new OTPAuth.TOTP({
issuer: otp.issuer,
label: otp.name,
algorithm: getAlgorithm(otp.algorithm),
digits: getDigits(otp.digits),
period: 30,
secret: encodedSecret,
});

export const getEncodedSecret = (otp?: OTP): string | undefined =>
otp?.secret && base32Encode(Uint8Array.from(Object.values(otp.secret)), "RFC4648");

0 comments on commit 945ea90

Please sign in to comment.