Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add tacktracker zip download capability #34

Merged
merged 6 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/assets/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"callsignsetup-callsign-allowed-chars": "Allowed characters: a-z, A-Z, 0-9",
"callsignsetup-give-your-callsign": "Enter your callsign.",
"callsignsetup-enter-callsign-title": "Your Callsign?",
"callsignsetup-enter-callsign-detail": "The callsign is your identifier in the services. Provide the callsign you have been assigned to.",
"callsignsetup-enter-callsign-detail": "Welcome! You are creating your identity to this service. The callsign will be your identifier. Provide the callsign you have been assigned to.",
"callsignsetup-your-callsign": "Set your callsign",

"ENROLLMENTVIEW": "ENROLLMENTVIEW",
Expand Down Expand Up @@ -274,8 +274,9 @@
"takZipDownload.downloadErrorDescription": "Downloading your Client Package failed. Please try again later.",
"takZipDownload.errorFromApplication": "Error message from the application:",
"takZipDownload.genericErrorMessage": "Unidentified error.",
"takZipDownload.androidOrWin": "Android ATAK or Windows WinTAK",
"takZipDownload.osOptions.androidOrWin": "Android ATAK or Windows WinTAK",
"takZipDownload.osOptions.ios": "iOS ITAK",
"takZipDownload.osOptions.tracker": "TAK Tracker package",
"takZipDownload.iAmDownloading": "Fetching your package...",

"SERVICETAKUSAGECARD": "SERVICETAKUSAGECARD",
Expand Down
5 changes: 3 additions & 2 deletions src/assets/locale/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"callsignsetup-callsign-allowed-chars": "Peitenimen maksimipituus on 30 merkkiä",
"callsignsetup-give-your-callsign": "Anna peitenimesi.",
"callsignsetup-enter-callsign-title": "Peitenimesi?",
"callsignsetup-enter-callsign-detail": "Peitenimi on tunnisteesi palveluissa. Anna sinulle käsketyn mukainen peitenimi.",
"callsignsetup-enter-callsign-detail": "Tervetuloa! Olet luomassa identiteettiä tähän palveluun. Peitenimi on tunnisteesi palveluissa. Anna sinulle käsketyn mukainen peitenimi.",
"callsignsetup-your-callsign": "Peitenimesi",

"ENROLLMENTVIEW": "ENROLLMENTVIEW",
Expand Down Expand Up @@ -274,8 +274,9 @@
"takZipDownload.downloadErrorDescription": "Viestiperustepaketin lataaminen epäonnistui. Yritä myöhemmin uudelleen.",
"takZipDownload.errorFromApplication": "Virheilmoitus sovellukselta:",
"takZipDownload.genericErrorMessage": "Tunnistamaton virhe.",
"takZipDownload.androidOrWin": "Android ATAK tai Windows WinTAK",
"takZipDownload.osOptions.androidOrWin": "Android ATAK tai Windows WinTAK",
"takZipDownload.osOptions.ios": "iOS ITAK",
"takZipDownload.osOptions.tracker": "TAK Tracker",
"takZipDownload.iAmDownloading": "Hankin perustepakettiasi...",

"SERVICETAKUSAGECARD": "SERVICETAKUSAGECARD",
Expand Down
5 changes: 3 additions & 2 deletions src/components/tak/DownloadTakZipModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export function useDownloadTakZipModal(): DownloadTakZipModalReturn {
const osOptions: { label: string; value: string }[] = useMemo(
() => [
{ label: t("takZipDownload.osOptions.ios"), value: "iOS" },
{ label: t("takZipDownload.androidOrWin"), value: "Other" },
{ label: t("takZipDownload.osOptions.androidOrWin"), value: "Other" },
{ label: t("takZipDownload.osOptions.tracker"), value: "tracker" },
],
[t],
);
Expand Down Expand Up @@ -114,7 +115,7 @@ export function useDownloadTakZipModal(): DownloadTakZipModalReturn {
confirmColor: "success",
cancelLabel: t("go-back"),
onCancel: () => {
// just close the modal
// Just close the modal
},
});
}, [getDescriptionText, handleDownload, isLoading, openDialog, t]);
Expand Down
15 changes: 14 additions & 1 deletion src/hook/api/tak/useFetchZipFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,20 @@ async function fetchZipFile(
}

const data = (await res.json()) as FilesResponse;
const fileIndex = os === "iOS" ? 1 : 0; // iTAK package is at index 1

// Map OS to file indices: Other, iOS and tracker
const osToFileIndex: { [key: string]: number } = {
Other: 0, // ATAK package
iOS: 1, // iTAK package
tracker: 2, // Tracker package
};

const fileIndex = osToFileIndex[os];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be using a helper function which uses a switch case for safer implementation.

Although since the input value comes from the pre-defined options it doesn't matter that much.


if (fileIndex === undefined) {
throw new Error(`Unsupported OS: ${os}`);
}

const fileData = data.files.tak[fileIndex];

const byteCharacters = atob(fileData.data.split("base64,")[1]);
Expand Down
40 changes: 8 additions & 32 deletions src/views/login/EnrollmentView.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import { useEffect, useState, useMemo } from "react";
import { useEffect } from "react";
import QRCode from "react-qr-code";
import { useOwnEnrollmentStatus } from "../../hook/api/useOwnEnrollmentStatus";
import { useNavigate } from "react-router-dom";
import { UnfoldableCard } from "../../components/UnfoldableCard2";
import { Button } from "../../components/Button";
import { Layout } from "../../components/Layout";
import { useCopyToClipboard } from "../../hook/helpers/useCopyToClipboard";
import useHealthcheck from "../../hook/helpers/useHealthcheck";
import { CardsContainer } from "../../components/CardsContainer";
import { Text } from "../../components/Text";
import { useTranslation } from "react-i18next";
import { Trans } from "react-i18next";
import { useTranslation, Trans } from "react-i18next";

export function EnrollmentView() {
const { t } = useTranslation();
const navigate = useNavigate();
const { fqdn } = useHealthcheck();
const subdomain = useMemo(() => fqdn.split(".")[0], [fqdn]);
const { isCopied, copyError, handleCopy } = useCopyToClipboard();
const callsign = localStorage.getItem("callsign") ?? undefined;
const approveCode = localStorage.getItem("approveCode") ?? undefined;
Expand All @@ -26,42 +22,24 @@ export function EnrollmentView() {
callsign ?? ""
}&&approvalcode=${approveCode ?? ""}`;

// Redirect to login if approveCode or callsign is missing
useEffect(() => {
if (!approveCode || !callsign) {
navigate("/login");
}
}, [approveCode, callsign, navigate]);

const [isEnrolled, setIsEnrolled] = useState(false);
// Check enrollment status periodically and navigate on success
useOwnEnrollmentStatus({
onSuccess: (isEnrolled) => {
if (isEnrolled) {
setIsEnrolled(true);
onSuccess: (enrolled) => {
if (enrolled) {
navigate("/login/createmtls");
}
},
refetchInterval: 1000,
enabled: !isEnrolled,
});

if (isEnrolled) {
return (
<Layout showNavbar={true} showFooter={true}>
<main className="px-10 flex flex-col gap-3 items-center justify-start h-full">
<Text title={subdomain || "Loading..."} />
<span className="text-white">{t("enrollment-approved")}</span>
<Button
onClick={() => {
navigate("/login/createmtls");
window.location.reload();
}}
>
{t("continue-clicking-here")}
</Button>
</main>
</Layout>
);
}

// Render the waiting for approval view
return (
<Layout showNavbar={true} showFooter={false}>
<CardsContainer>
Expand Down Expand Up @@ -92,14 +70,12 @@ export function EnrollmentView() {
<div className="p-2 bg-white rounded-lg">
<QRCode value={approvalUrl} bgColor="#FFFFFF" />
</div>

<Text
title={callsign}
description={t("approval-code")}
description2={approveCode}
styling2="font-consolas"
/>

<UnfoldableCard
title={<Trans i18nKey="approval-waiting-title" />}
styling="bg-backgroundLight"
Expand Down
21 changes: 16 additions & 5 deletions src/views/login/LoginView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useMemo } from "react";
import { useContext, useState } from "react";
import { useMemo, useEffect, useContext, useState } from "react";
import { useLocation, useNavigate, Link } from "react-router-dom";
import { UserTypeContext } from "../../hook/auth/userTypeFetcher";
import { useCheckCode } from "../../hook/api/useCheckCode";
Expand Down Expand Up @@ -38,6 +37,7 @@ export function LoginView() {
const loginCodeStore = useLoginCodeStore();
const { deployment } = useHealthcheck();
const [codeNotValid, setCodeNotValid] = useState(false);
const [hasAutoSubmitted, setHasAutoSubmitted] = useState(false);
const protocol = window.location.protocol;
const host = window.location.host;
const mtlsUrl = `${protocol}//mtls.${host}/app/admin/`;
Expand All @@ -61,9 +61,13 @@ export function LoginView() {
},
});

// Destructure formik properties
const { values, isValid, submitForm, setFieldValue } = formik;

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const upperCaseValue = e.target.value.toUpperCase();
void formik.setFieldValue("code", upperCaseValue);
// Using void to intentionally ignore the Promise returned by setFieldValue, to suppress eslint
void setFieldValue("code", upperCaseValue, false);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the meaning of void here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring the promise. No async operation there so I think there's nothig that could fail here

};

const handleInputFocus = () => {
Expand All @@ -77,7 +81,7 @@ export function LoginView() {
error,
} = useCheckCode({
onSuccess: (data) => {
loginCodeStore.setCode(formik.values.code);
loginCodeStore.setCode(values.code);
if (data.isAdminCodeValid) {
loginCodeStore.setCodeType("admin");
setOtpVerified(true);
Expand All @@ -97,6 +101,13 @@ export function LoginView() {
},
});

useEffect(() => {
if (values.code && isValid && !isLoading && !hasAutoSubmitted) {
setHasAutoSubmitted(true);
void submitForm();
}
}, [values.code, isValid, isLoading, hasAutoSubmitted, submitForm]);

return (
<Layout showNavbar={false} showFooter={false} showPublicFooter={true}>
<CardsContainer>
Expand Down Expand Up @@ -138,7 +149,7 @@ export function LoginView() {
width: "full",
}}
type="submit"
disabled={!formik.isValid || isLoading}
disabled={!isValid || isLoading}
styling={buttonStyle}
>
<div className="flex items-center justify-center w-full h-full">
Expand Down