WhatsApp/Signal/Formstack/admin updates
This commit is contained in:
parent
bcecf61a46
commit
d0cc5a21de
451 changed files with 16139 additions and 39623 deletions
|
|
@ -34,6 +34,8 @@ export const Detail: FC<DetailProps> = ({ service, row }) => {
|
|||
const { almostBlack } = colors;
|
||||
const { bodyLarge } = typography;
|
||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
||||
const [showRelinkConfirmation, setShowRelinkConfirmation] = useState(false);
|
||||
const [isRelinking, setIsRelinking] = useState(false);
|
||||
|
||||
const continueDeleteAction = async () => {
|
||||
await deleteAction?.(id);
|
||||
|
|
@ -41,6 +43,23 @@ export const Detail: FC<DetailProps> = ({ service, row }) => {
|
|||
router.push(`${getBasePath()}${entity}`);
|
||||
};
|
||||
|
||||
const continueRelinkAction = async () => {
|
||||
setIsRelinking(true);
|
||||
try {
|
||||
const response = await fetch(`/link/api/${entity}/bots/${token}/relink`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (response.ok) {
|
||||
setShowRelinkConfirmation(false);
|
||||
router.refresh();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Relink failed:", error);
|
||||
} finally {
|
||||
setIsRelinking(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
|
|
@ -57,6 +76,15 @@ export const Detail: FC<DetailProps> = ({ service, row }) => {
|
|||
onClick={() => setShowDeleteConfirmation(true)}
|
||||
/>
|
||||
</Grid>
|
||||
{service === "whatsapp" && (
|
||||
<Grid item>
|
||||
<Button
|
||||
text="Relink"
|
||||
kind="secondary"
|
||||
onClick={() => setShowRelinkConfirmation(true)}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item>
|
||||
<Button
|
||||
text="Edit"
|
||||
|
|
@ -129,6 +157,35 @@ export const Detail: FC<DetailProps> = ({ service, row }) => {
|
|||
Are you sure you want to delete this record?
|
||||
</Box>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
open={showRelinkConfirmation}
|
||||
size="xs"
|
||||
title="Relink WhatsApp Connection?"
|
||||
buttons={
|
||||
<Grid container justifyContent="space-between">
|
||||
<Grid item>
|
||||
<Button
|
||||
text="Cancel"
|
||||
kind="secondary"
|
||||
onClick={() => setShowRelinkConfirmation(false)}
|
||||
disabled={isRelinking}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
text={isRelinking ? "Relinking..." : "Relink"}
|
||||
kind="primary"
|
||||
onClick={continueRelinkAction}
|
||||
disabled={isRelinking}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
<Box sx={{ ...bodyLarge, color: almostBlack }}>
|
||||
This will disconnect the current WhatsApp link and generate a new QR code. You will need to scan the new QR code to reconnect. Continue?
|
||||
</Box>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { FC, useEffect, useState } from "react";
|
||||
// @ts-ignore - react-qr-code doesn't have React 19 compatible types yet
|
||||
import QRCodeInternal from "react-qr-code";
|
||||
import { Box } from "@mui/material";
|
||||
import { colors } from "../styles/theme";
|
||||
|
|
@ -28,22 +29,30 @@ export const QRCode: FC<QRCodeProps> = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (!verified && getValue && refreshInterval) {
|
||||
const interval = setInterval(async () => {
|
||||
// Fetch immediately on mount
|
||||
const fetchQR = async () => {
|
||||
const { qr, kind } = await getValue(token);
|
||||
console.log({ kind });
|
||||
setValue(qr);
|
||||
setKind(kind);
|
||||
}, refreshInterval * 1000);
|
||||
};
|
||||
fetchQR();
|
||||
|
||||
// Then set up interval for refreshes
|
||||
const interval = setInterval(fetchQR, refreshInterval * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [getValue, refreshInterval]);
|
||||
}, [getValue, refreshInterval, token, verified]);
|
||||
|
||||
return !verified ? (
|
||||
<Box sx={{ backgroundColor: white, m: 2 }}>
|
||||
{kind === "data" ? (
|
||||
<QRCodeInternal value={value} />
|
||||
{value ? (
|
||||
kind === "data" ? (
|
||||
<QRCodeInternal value={value} />
|
||||
) : (
|
||||
<img src={value} alt={name} />
|
||||
)
|
||||
) : (
|
||||
<img src={value} alt={name} />
|
||||
<Box>Loading QR code...</Box>
|
||||
)}
|
||||
<Box>{helperText}</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -3,18 +3,19 @@ type ServiceLayoutProps = {
|
|||
detail: any;
|
||||
edit: any;
|
||||
create: any;
|
||||
params: {
|
||||
params: Promise<{
|
||||
segment: string[];
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export const ServiceLayout = ({
|
||||
export const ServiceLayout = async ({
|
||||
children,
|
||||
detail,
|
||||
edit,
|
||||
create,
|
||||
params: { segment },
|
||||
params,
|
||||
}: ServiceLayoutProps) => {
|
||||
const { segment } = await params;
|
||||
const length = segment?.length ?? 0;
|
||||
const isCreate = length === 2 && segment[1] === "create";
|
||||
const isEdit = length === 3 && segment[2] === "edit";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { ServiceConfig } from "../lib/service";
|
||||
|
||||
const getQRCode = async (token: string): Promise<Record<string, string>> => {
|
||||
const url = `/link/api/signal/bots/${token}`;
|
||||
const basePath = window?.location?.pathname?.startsWith("/link")
|
||||
? "/link"
|
||||
: "";
|
||||
const url = `${basePath}/api/signal/bots/${token}`;
|
||||
const result = await fetch(url, { cache: "no-store" });
|
||||
const { qr } = await result.json();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,28 @@ import { ServiceConfig } from "../lib/service";
|
|||
// import { generateSelectOneAction } from "../lib/actions";
|
||||
|
||||
const getQRCode = async (token: string) => {
|
||||
const url = `/link/api/whatsapp/bots/${token}`;
|
||||
const result = await fetch(url, { cache: "no-store" });
|
||||
const { qr } = await result.json();
|
||||
try {
|
||||
const url = `/link/api/whatsapp/bots/${token}`;
|
||||
const result = await fetch(url, { cache: "no-store" });
|
||||
|
||||
return { qr, kind: "data" };
|
||||
if (!result.ok) {
|
||||
console.error(`Failed to fetch QR code: ${result.status} ${result.statusText}`);
|
||||
return { qr: "", kind: "data" };
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
const { qr } = data;
|
||||
|
||||
if (!qr) {
|
||||
console.error("No QR code in response");
|
||||
return { qr: "", kind: "data" };
|
||||
}
|
||||
|
||||
return { qr, kind: "data" };
|
||||
} catch (error) {
|
||||
console.error("Error fetching QR code:", error);
|
||||
return { qr: "", kind: "data" };
|
||||
}
|
||||
};
|
||||
|
||||
export const whatsappConfig: ServiceConfig = {
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ export {
|
|||
sendMessage,
|
||||
receiveMessage,
|
||||
handleWebhook,
|
||||
relinkBot,
|
||||
} from "./lib/routing";
|
||||
|
|
|
|||
|
|
@ -5,19 +5,26 @@ import { getService } from "./utils";
|
|||
export const getBot = async (
|
||||
_req: NextRequest,
|
||||
params: ServiceParams,
|
||||
): Promise<NextResponse> => getService(params)?.getBot(params);
|
||||
): Promise<NextResponse> => (await getService(params))?.getBot(params);
|
||||
|
||||
export const sendMessage = async (
|
||||
req: NextRequest,
|
||||
params: ServiceParams,
|
||||
): Promise<NextResponse> => getService(params)?.sendMessage(req, params);
|
||||
): Promise<NextResponse> =>
|
||||
(await getService(params))?.sendMessage(req, params);
|
||||
|
||||
export const receiveMessage = async (
|
||||
req: NextRequest,
|
||||
params: ServiceParams,
|
||||
): Promise<NextResponse> => getService(params)?.receiveMessage(req, params);
|
||||
): Promise<NextResponse> =>
|
||||
(await getService(params))?.receiveMessage(req, params);
|
||||
|
||||
export const handleWebhook = async (
|
||||
req: NextRequest,
|
||||
params: ServiceParams,
|
||||
): Promise<NextResponse> => getService(params)?.handleWebhook(req);
|
||||
): Promise<NextResponse> => (await getService(params))?.handleWebhook(req);
|
||||
|
||||
export const relinkBot = async (
|
||||
_req: NextRequest,
|
||||
params: ServiceParams,
|
||||
): Promise<NextResponse> => (await getService(params))?.relink(params);
|
||||
|
|
|
|||
|
|
@ -51,16 +51,15 @@ export type ServiceConfig = {
|
|||
};
|
||||
|
||||
export type ServiceParams = {
|
||||
params: {
|
||||
params: Promise<{
|
||||
service: string;
|
||||
token?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export class Service {
|
||||
async getBot({
|
||||
params: { service, token },
|
||||
}: ServiceParams): Promise<NextResponse> {
|
||||
async getBot({ params }: ServiceParams): Promise<NextResponse> {
|
||||
const { service, token } = await params;
|
||||
const table = getServiceTable(service);
|
||||
const row = await db
|
||||
.selectFrom(table)
|
||||
|
|
@ -71,16 +70,15 @@ export class Service {
|
|||
return NextResponse.json(row);
|
||||
}
|
||||
|
||||
async registerBot({
|
||||
params: { service, token },
|
||||
}: ServiceParams): Promise<NextResponse> {
|
||||
async registerBot({ params: _params }: ServiceParams): Promise<NextResponse> {
|
||||
return NextResponse.error() as any;
|
||||
}
|
||||
|
||||
async sendMessage(
|
||||
req: NextRequest,
|
||||
{ params: { service, token } }: ServiceParams,
|
||||
{ params }: ServiceParams,
|
||||
): Promise<NextResponse> {
|
||||
const { service, token } = await params;
|
||||
const table = getServiceTable(service);
|
||||
const row = await db
|
||||
.selectFrom(table)
|
||||
|
|
@ -103,14 +101,14 @@ export class Service {
|
|||
},
|
||||
};
|
||||
|
||||
console.log(response);
|
||||
return NextResponse.json(response);
|
||||
}
|
||||
|
||||
async receiveMessage(
|
||||
req: NextRequest,
|
||||
{ params: { service, token } }: ServiceParams,
|
||||
{ params }: ServiceParams,
|
||||
): Promise<NextResponse> {
|
||||
const { service, token } = await params;
|
||||
const json = await req.json();
|
||||
const worker = await getWorkerUtils();
|
||||
await worker.addJob(`${service}/receive-${service}-message`, {
|
||||
|
|
@ -124,4 +122,8 @@ export class Service {
|
|||
async handleWebhook(_req: NextRequest): Promise<NextResponse> {
|
||||
return NextResponse.error() as any;
|
||||
}
|
||||
|
||||
async relink({ params: _params }: ServiceParams): Promise<NextResponse> {
|
||||
return NextResponse.error() as any;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ const fetchNoCache = async (url: string, options = {}) => {
|
|||
};
|
||||
|
||||
export class Signal extends Service {
|
||||
async getBot({ params: { token } }: ServiceParams) {
|
||||
async getBot({ params }: ServiceParams) {
|
||||
const { token } = await params;
|
||||
const row = await db
|
||||
.selectFrom("SignalBot")
|
||||
.selectAll()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { Facebook } from "./facebook";
|
|||
import { Signal } from "./signal";
|
||||
import { Whatsapp } from "./whatsapp";
|
||||
|
||||
export const getService = ({ params: { service } }: ServiceParams): Service => {
|
||||
export const getService = async ({
|
||||
params,
|
||||
}: ServiceParams): Promise<Service> => {
|
||||
const { service } = await params;
|
||||
if (service === "facebook") {
|
||||
return new Facebook();
|
||||
} else if (service === "signal") {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { revalidatePath } from "next/cache";
|
|||
import { Service, ServiceParams } from "./service";
|
||||
|
||||
export class Whatsapp extends Service {
|
||||
async getBot({ params: { token } }: ServiceParams) {
|
||||
async getBot({ params }: ServiceParams) {
|
||||
const { token } = await params;
|
||||
const row = await db
|
||||
.selectFrom("WhatsappBot")
|
||||
.selectAll()
|
||||
|
|
@ -30,4 +31,30 @@ export class Whatsapp extends Service {
|
|||
|
||||
return NextResponse.json(json);
|
||||
}
|
||||
|
||||
async relink({ params }: ServiceParams) {
|
||||
const { token } = await params;
|
||||
const row = await db
|
||||
.selectFrom("WhatsappBot")
|
||||
.selectAll()
|
||||
.where("token", "=", token as string)
|
||||
.executeTakeFirstOrThrow();
|
||||
const id = row.id;
|
||||
|
||||
// Step 1: Call unverify to remove the bot directory and disconnect
|
||||
const unverifyUrl = `${process.env.BRIDGE_WHATSAPP_URL}/api/bots/${id}/unverify`;
|
||||
await fetch(unverifyUrl, { method: "POST" });
|
||||
|
||||
// Step 2: Reset verified flag in database
|
||||
await db
|
||||
.updateTable("WhatsappBot")
|
||||
.set({ verified: false })
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
|
||||
// Step 3: Revalidate the path to refresh the UI
|
||||
revalidatePath(`/whatsapp/${id}`);
|
||||
|
||||
return NextResponse.json({ success: true, message: "WhatsApp connection reset. Please scan the new QR code." });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
{
|
||||
"name": "@link-stack/bridge-ui",
|
||||
"version": "2.2.0",
|
||||
"version": "3.3.0",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@link-stack/bridge-common": "^2.2.0",
|
||||
"@link-stack/signal-api": "*",
|
||||
"@link-stack/ui": "^2.2.0",
|
||||
"@mui/material": "^5",
|
||||
"@mui/x-data-grid-pro": "^7.18.0",
|
||||
"kysely": "0.26.1",
|
||||
"next": "^14.2.25",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-qr-code": "^2.0.15"
|
||||
"@link-stack/bridge-common": "workspace:*",
|
||||
"@link-stack/signal-api": "workspace:*",
|
||||
"@link-stack/ui": "workspace:*",
|
||||
"@mui/material": "^6",
|
||||
"@mui/x-data-grid-pro": "^7",
|
||||
"kysely": "0.27.5",
|
||||
"next": "15.5.4",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-qr-code": "^2.0.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.7.3",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"typescript": "5.6.2"
|
||||
"@types/node": "^24.7.0",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ export const colors: any = {
|
|||
helpYellow: "#fff4d5",
|
||||
dwcDarkBlue: "#191847",
|
||||
hazyMint: "#ecf7f8",
|
||||
leafcutterElectricBlue: "#4d6aff",
|
||||
leafcutterLightBlue: "#fafbfd",
|
||||
waterbearElectricPurple: "#332c83",
|
||||
waterbearLightSmokePurple: "#eff3f8",
|
||||
bumpedPurple: "#212058",
|
||||
|
|
|
|||
|
|
@ -1,26 +1,32 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": false,
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*", "../../node_modules/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": ["**.d.ts", "**/*.ts", "**/*.tsx", "**/*.png, **/*.svg"],
|
||||
"exclude": ["node_modules", "babel__core", "dist"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue