mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-02 21:19:12 +00:00
Merge pull request #876 from khoj-ai/features/use-intl-phone-input-settings
Use international phone number input and verify whatsapp flow
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import styles from "./settings.module.css";
|
import styles from "./settings.module.css";
|
||||||
|
import "intl-tel-input/styles";
|
||||||
|
|
||||||
import { Suspense, useEffect, useState } from "react";
|
import { Suspense, useEffect, useState } from "react";
|
||||||
import { useToast } from "@/components/ui/use-toast"
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
@@ -74,6 +75,8 @@ import NavMenu from "../components/navMenu/navMenu";
|
|||||||
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
|
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
|
||||||
import Loading from "../components/loading/loading";
|
import Loading from "../components/loading/loading";
|
||||||
|
|
||||||
|
import IntlTelInput from 'intl-tel-input/react';
|
||||||
|
|
||||||
|
|
||||||
const ManageFilesModal: React.FC<{ onClose: () => void }> = ({ onClose }) => {
|
const ManageFilesModal: React.FC<{ onClose: () => void }> = ({ onClose }) => {
|
||||||
const [syncedFiles, setSyncedFiles] = useState<string[]>([]);
|
const [syncedFiles, setSyncedFiles] = useState<string[]>([]);
|
||||||
@@ -349,7 +352,7 @@ export default function SettingsView() {
|
|||||||
const [userConfig, setUserConfig] = useState<UserConfig | null>(null);
|
const [userConfig, setUserConfig] = useState<UserConfig | null>(null);
|
||||||
const [name, setName] = useState<string | undefined>(undefined);
|
const [name, setName] = useState<string | undefined>(undefined);
|
||||||
const [notionToken, setNotionToken] = useState<string | null>(null);
|
const [notionToken, setNotionToken] = useState<string | null>(null);
|
||||||
const [number, setNumber] = useState<string | undefined>(undefined);
|
const [phoneNumber, setPhoneNumber] = useState<string | undefined>(undefined);
|
||||||
const [otp, setOTP] = useState("");
|
const [otp, setOTP] = useState("");
|
||||||
const [numberValidationState, setNumberValidationState] = useState<PhoneNumberValidationState>(PhoneNumberValidationState.Verified);
|
const [numberValidationState, setNumberValidationState] = useState<PhoneNumberValidationState>(PhoneNumberValidationState.Verified);
|
||||||
const [isManageFilesModalOpen, setIsManageFilesModalOpen] = useState(false);
|
const [isManageFilesModalOpen, setIsManageFilesModalOpen] = useState(false);
|
||||||
@@ -358,7 +361,7 @@ export default function SettingsView() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUserConfig(initialUserConfig);
|
setUserConfig(initialUserConfig);
|
||||||
setNumber(initialUserConfig?.phone_number);
|
setPhoneNumber(initialUserConfig?.phone_number);
|
||||||
setNumberValidationState(
|
setNumberValidationState(
|
||||||
initialUserConfig?.is_phone_number_verified
|
initialUserConfig?.is_phone_number_verified
|
||||||
? PhoneNumberValidationState.Verified
|
? PhoneNumberValidationState.Verified
|
||||||
@@ -379,7 +382,7 @@ export default function SettingsView() {
|
|||||||
|
|
||||||
const sendOTP = async () => {
|
const sendOTP = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/phone?phone_number=${number}`, {
|
const response = await fetch(`/api/phone?phone_number=${phoneNumber}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -421,6 +424,31 @@ export default function SettingsView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const disconnectNumber = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/phone`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to disconnect phone number');
|
||||||
|
|
||||||
|
setPhoneNumber(undefined);
|
||||||
|
setNumberValidationState(PhoneNumberValidationState.Setup);
|
||||||
|
toast({
|
||||||
|
title: "📱 Phone",
|
||||||
|
description: "Phone number disconnected",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error disconnecting phone number:', error);
|
||||||
|
toast({
|
||||||
|
title: "📱 Phone",
|
||||||
|
description: "Failed to disconnect phone number. Try again or contact us at team@khoj.dev",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const setSubscription = async (state: string) => {
|
const setSubscription = async (state: string) => {
|
||||||
try {
|
try {
|
||||||
const url = `/api/subscription?email=${userConfig?.username}&operation=${state}`;
|
const url = `/api/subscription?email=${userConfig?.username}&operation=${state}`;
|
||||||
@@ -942,15 +970,26 @@ export default function SettingsView() {
|
|||||||
Connect your number to chat with Khoj on WhatsApp. Learn more about the integration <a href="https://docs.khoj.dev/clients/whatsapp">here</a>.
|
Connect your number to chat with Khoj on WhatsApp. Learn more about the integration <a href="https://docs.khoj.dev/clients/whatsapp">here</a>.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
|
<IntlTelInput
|
||||||
|
initialValue={phoneNumber || ""}
|
||||||
|
onChangeNumber={setPhoneNumber}
|
||||||
|
disabled={numberValidationState === PhoneNumberValidationState.VerifyOTP}
|
||||||
|
initOptions={{
|
||||||
|
separateDialCode: true,
|
||||||
|
initialCountry: "af",
|
||||||
|
utilsScript: "https://assets.khoj.dev/intl-tel-input%4023.8.0_build_js_utils.js",
|
||||||
|
containerClass: `${styles.phoneInput}`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{numberValidationState === PhoneNumberValidationState.VerifyOTP && (
|
{numberValidationState === PhoneNumberValidationState.VerifyOTP && (
|
||||||
<>
|
<>
|
||||||
<p>{`Enter the OTP sent to your WhatsApp number: ${number}`}</p>
|
<p>{`Enter the OTP sent to your number: ${phoneNumber}`}</p>
|
||||||
<InputOTP
|
<InputOTP
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
value={otp || ""}
|
value={otp || ""}
|
||||||
onChange={setOTP}
|
onChange={setOTP}
|
||||||
onComplete={() => setNumberValidationState(PhoneNumberValidationState.Verified)}
|
onComplete={() => setNumberValidationState(PhoneNumberValidationState.VerifyOTP)}
|
||||||
>
|
>
|
||||||
<InputOTPGroup>
|
<InputOTPGroup>
|
||||||
<InputOTPSlot index={0} />
|
<InputOTPSlot index={0} />
|
||||||
@@ -962,16 +1001,6 @@ export default function SettingsView() {
|
|||||||
</InputOTPGroup>
|
</InputOTPGroup>
|
||||||
</InputOTP>
|
</InputOTP>
|
||||||
</>
|
</>
|
||||||
) || (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
type="tel"
|
|
||||||
onChange={(e) => setNumber(e.target.value)}
|
|
||||||
value={number || ""}
|
|
||||||
placeholder="Enter phone number (e.g. +911234567890)"
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-6"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -986,17 +1015,27 @@ export default function SettingsView() {
|
|||||||
) || (
|
) || (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!number || number === userConfig.phone_number || !isValidPhoneNumber(number)}
|
disabled={!phoneNumber || (phoneNumber === userConfig.phone_number && numberValidationState === PhoneNumberValidationState.Verified) || !isValidPhoneNumber(phoneNumber)}
|
||||||
onClick={sendOTP}
|
onClick={sendOTP}
|
||||||
>
|
>
|
||||||
{!userConfig.phone_number
|
{!userConfig.phone_number
|
||||||
? (<><Plugs className="inline mr-2" />Setup Whatsapp</>)
|
? (<><Plugs className="inline mr-2" />Setup Whatsapp</>)
|
||||||
: !number || number === userConfig.phone_number || !isValidPhoneNumber(number)
|
: !phoneNumber || (phoneNumber === userConfig.phone_number && numberValidationState === PhoneNumberValidationState.Verified) || !isValidPhoneNumber(phoneNumber)
|
||||||
? (<><PlugsConnected className="inline mr-2 text-green-400" />Switch Number</>)
|
? (<><PlugsConnected className="inline mr-2 text-green-400" />Switch Number</>)
|
||||||
: (<>Send OTP to Whatsapp <ArrowRight className="inline ml-2" weight="bold"/></>)
|
: (<>Send OTP <ArrowRight className="inline ml-2" weight="bold"/></>)
|
||||||
}
|
}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{
|
||||||
|
numberValidationState === PhoneNumberValidationState.Verified && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => disconnectNumber()}
|
||||||
|
>
|
||||||
|
<CloudSlash className="h-5 w-5 mr-2" />Disconnect
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,3 +9,14 @@ div.contentBody {
|
|||||||
display: grid;
|
display: grid;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.phoneInput {
|
||||||
|
padding: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.phoneInput input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid hsla(var(--border));
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.3",
|
"eslint-config-next": "14.2.3",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
|
"intl-tel-input": "^23.8.0",
|
||||||
"katex": "^0.16.10",
|
"katex": "^0.16.10",
|
||||||
"libphonenumber-js": "^1.11.4",
|
"libphonenumber-js": "^1.11.4",
|
||||||
"lucide-react": "^0.397.0",
|
"lucide-react": "^0.397.0",
|
||||||
@@ -64,7 +65,13 @@
|
|||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
|
"@types/intl-tel-input": "^18.1.4",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
|
"@types/markdown-it": "^14.1.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.3",
|
"eslint-config-next": "14.2.3",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
@@ -73,15 +80,10 @@
|
|||||||
"lint-staged": "^15.2.7",
|
"lint-staged": "^15.2.7",
|
||||||
"nodemon": "^3.1.3",
|
"nodemon": "^3.1.3",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss": "^3.4.6",
|
"tailwindcss": "^3.4.6",
|
||||||
"typescript": "^5",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"@types/dompurify": "^3.0.5",
|
"typescript": "^5"
|
||||||
"@types/katex": "^0.16.7",
|
|
||||||
"@types/markdown-it": "^14.1.1",
|
|
||||||
"@types/react": "^18",
|
|
||||||
"@types/react-dom": "^18"
|
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"tabWidth": 4
|
"tabWidth": 4
|
||||||
|
|||||||
@@ -1295,6 +1295,20 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/trusted-types" "*"
|
"@types/trusted-types" "*"
|
||||||
|
|
||||||
|
"@types/intl-tel-input@^18.1.4":
|
||||||
|
version "18.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/intl-tel-input/-/intl-tel-input-18.1.4.tgz#0eb5211a7490f8a8d7aa940ee594a85138d514c9"
|
||||||
|
integrity sha512-UT4dQ4dQA0w0uxU161aPROEEwMOQj5Qedm3ImdDNfkxi3JXzBX2FhcUS72pTyZ8ypaUr2e9ruJBiK6bwSGfbew==
|
||||||
|
dependencies:
|
||||||
|
"@types/jquery" "*"
|
||||||
|
|
||||||
|
"@types/jquery@*":
|
||||||
|
version "3.5.30"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.30.tgz#888d584cbf844d3df56834b69925085038fd80f7"
|
||||||
|
integrity sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==
|
||||||
|
dependencies:
|
||||||
|
"@types/sizzle" "*"
|
||||||
|
|
||||||
"@types/json5@^0.0.29":
|
"@types/json5@^0.0.29":
|
||||||
version "0.0.29"
|
version "0.0.29"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
@@ -1350,6 +1364,11 @@
|
|||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/sizzle@*":
|
||||||
|
version "2.3.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627"
|
||||||
|
integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==
|
||||||
|
|
||||||
"@types/trusted-types@*":
|
"@types/trusted-types@*":
|
||||||
version "2.0.7"
|
version "2.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||||
@@ -2982,6 +3001,11 @@ internal-slot@^1.0.4, internal-slot@^1.0.7:
|
|||||||
hasown "^2.0.0"
|
hasown "^2.0.0"
|
||||||
side-channel "^1.0.4"
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
|
intl-tel-input@^23.8.0:
|
||||||
|
version "23.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/intl-tel-input/-/intl-tel-input-23.8.0.tgz#37bec095605516aa72529b3da11335b253b65b2f"
|
||||||
|
integrity sha512-lx8Sz5LfVyIXyjWbfjno89o4qHfIuWulctGaWbP2RKKnHvagdt9gdibCsv9uEH7izb/yjB6Nst0sRo988/lhpw==
|
||||||
|
|
||||||
invariant@^2.2.4:
|
invariant@^2.2.4:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||||
@@ -4442,6 +4466,7 @@ string-argv@~0.3.2:
|
|||||||
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
|
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
||||||
|
name string-width-cjs
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
|||||||
@@ -1298,7 +1298,7 @@ def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False)
|
|||||||
"user_photo": user_picture,
|
"user_photo": user_picture,
|
||||||
"is_active": is_active,
|
"is_active": is_active,
|
||||||
"given_name": given_name,
|
"given_name": given_name,
|
||||||
"phone_number": user.phone_number,
|
"phone_number": str(user.phone_number) if user.phone_number else "",
|
||||||
"is_phone_number_verified": user.verified_phone_number,
|
"is_phone_number_verified": user.verified_phone_number,
|
||||||
# user content settings
|
# user content settings
|
||||||
"enabled_content_source": enabled_content_sources,
|
"enabled_content_source": enabled_content_sources,
|
||||||
|
|||||||
Reference in New Issue
Block a user