From 57cd0eda046529bfbc760bb007395e90c8a1d485 Mon Sep 17 00:00:00 2001 From: Modar Abo Hassoun Date: Sat, 4 Jul 2026 12:45:12 +0200 Subject: [PATCH 1/7] feat: add phone number column to user table closes #751 --- seerr-api.yml | 2 + server/entity/User.ts | 3 + .../1783161650264-AddPhoneNumberToUser.ts | 15 ++++ .../1783161620407-AddPhoneNumberToUser.ts | 83 +++++++++++++++++++ server/routes/user/index.ts | 1 + 5 files changed, 104 insertions(+) create mode 100644 server/migration/postgres/1783161650264-AddPhoneNumberToUser.ts create mode 100644 server/migration/sqlite/1783161620407-AddPhoneNumberToUser.ts diff --git a/seerr-api.yml b/seerr-api.yml index cd0012b679..86278c698c 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -4264,6 +4264,8 @@ paths: type: string permissions: type: number + phoneNumber: + type: string responses: '201': description: The created user diff --git a/server/entity/User.ts b/server/entity/User.ts index 644b38893f..0d3ffc6dc6 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -77,6 +77,9 @@ export class User { @Column({ nullable: true, select: false }) public password?: string; + @Column({ type: 'varchar', nullable: true, length: 20 }) + public phoneNumber?: string; + @Column({ nullable: true, select: false }) public resetPasswordGuid?: string; diff --git a/server/migration/postgres/1783161650264-AddPhoneNumberToUser.ts b/server/migration/postgres/1783161650264-AddPhoneNumberToUser.ts new file mode 100644 index 0000000000..8210c7a1ee --- /dev/null +++ b/server/migration/postgres/1783161650264-AddPhoneNumberToUser.ts @@ -0,0 +1,15 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPhoneNumberToUser1783161650264 implements MigrationInterface { + name = 'AddPhoneNumberToUser1783161650264'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ADD "phoneNumber" character varying(20)` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "phoneNumber"`); + } +} diff --git a/server/migration/sqlite/1783161620407-AddPhoneNumberToUser.ts b/server/migration/sqlite/1783161620407-AddPhoneNumberToUser.ts new file mode 100644 index 0000000000..9bcf3b4080 --- /dev/null +++ b/server/migration/sqlite/1783161620407-AddPhoneNumberToUser.ts @@ -0,0 +1,83 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPhoneNumberToUser1783161620407 implements MigrationInterface { + name = 'AddPhoneNumberToUser1783161620407'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + await queryRunner.query( + `CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" datetime, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, "avatarETag" varchar, "avatarVersion" varchar, "phoneNumber" varchar(20), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion" FROM "user"` + ); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`); + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`); + await queryRunner.query( + `CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" datetime, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, "avatarETag" varchar, "avatarVersion" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))` + ); + await queryRunner.query( + `INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion" FROM "temporary_user"` + ); + await queryRunner.query(`DROP TABLE "temporary_user"`); + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + } +} diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 0d5843f85a..8cc646aa23 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -211,6 +211,7 @@ router.post( permissions: settings.main.defaultPermissions, plexToken: '', userType: UserType.LOCAL, + phoneNumber: body.phoneNumber, }); if (passedExplicitPassword) { From 0adb3651fba4238a059b0e2e48dee942af3da80f Mon Sep 17 00:00:00 2001 From: Modar Abo Hassoun Date: Sun, 5 Jul 2026 17:54:35 +0200 Subject: [PATCH 2/7] feat(user): add phone number input --- src/components/UserList/index.tsx | 25 +++++++++++++++++++++++++ src/i18n/locale/de.json | 2 ++ src/i18n/locale/en.json | 2 ++ src/styles/globals.css | 1 + 4 files changed, 30 insertions(+) diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index c36d7d692a..e7323067f7 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -43,6 +43,8 @@ import validator from 'validator'; import * as Yup from 'yup'; import JellyfinImportModal from './JellyfinImportModal'; +const PHONE_REGEX = /^\+?[0-9]{1,15}$/; + const messages = defineMessages('components.UserList', { users: 'Users', userlist: 'User List', @@ -75,6 +77,8 @@ const messages = defineMessages('components.UserList', { username: 'Username', email: 'Email Address', password: 'Password', + phoneNumber: 'Phone number', + validationPhoneNumber: 'Invalid phone number', passwordinfodescription: 'Configure an application URL and enable email notifications to allow automatic password generation.', autogeneratepassword: 'Automatically Generate Password', @@ -326,6 +330,10 @@ const UserList = () => { intl.formatMessage(messages.validationpasswordminchars) ) ), + phoneNumber: Yup.string().matches(PHONE_REGEX, { + message: intl.formatMessage(messages.validationPhoneNumber), + excludeEmptyString: true, + }), }); if (!data) { @@ -386,6 +394,7 @@ const UserList = () => { username: '', email: '', password: '', + phoneNumber: '', genpassword: false, }} validationSchema={CreateUserSchema} @@ -395,6 +404,7 @@ const UserList = () => { username: values.username, email: values.email, password: values.genpassword ? null : values.password, + phoneNumber: values.phoneNumber, }); addToast(intl.formatMessage(messages.usercreatedsuccess), { appearance: 'success', @@ -554,6 +564,21 @@ const UserList = () => { )} +
+ +
+
+ +
+ {errors.phoneNumber && + touched.phoneNumber && + typeof errors.phoneNumber === 'string' && ( +
{errors.phoneNumber}
+ )} +
+
); diff --git a/src/i18n/locale/de.json b/src/i18n/locale/de.json index b464f227ff..ccf0926731 100644 --- a/src/i18n/locale/de.json +++ b/src/i18n/locale/de.json @@ -1660,6 +1660,8 @@ "components.UserList.sortByUser": "Nach Benutzername sortieren", "components.UserList.toggleSortDirection": "Erneut klicken, um {direction} zu sortieren", "components.UserList.toggleSortDirectionAria": "Sortierreihenfolge umschalten", + "components.UserList.phoneNumber": "Telefonnummer", + "components.UserList.validationPhoneNumber": "Ungültige Telefonnummer", "components.UserProfile.UserSettings.UserPasswordChange.localPasswordDescription": "Dieses Passwort wird für die lokale Anmeldung bei {applicationTitle} verwendet. Es ist unabhängig von deinem Medienserver-Passwort.", "i18n.error": "Etwas ist schiefgelaufen. Bitte versuche es erneut.", "i18n.notificationLocale": "Benachrichtigungssprache", diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 2a880430fc..1f63f04303 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1427,6 +1427,7 @@ "components.UserList.owner": "Owner", "components.UserList.password": "Password", "components.UserList.passwordinfodescription": "Configure an application URL and enable email notifications to allow automatic password generation.", + "components.UserList.phoneNumber": "Phone number", "components.UserList.plexuser": "Plex User", "components.UserList.role": "Role", "components.UserList.sortBy": "Sort by {field}", @@ -1451,6 +1452,7 @@ "components.UserList.users": "Users", "components.UserList.userssaved": "User permissions saved successfully!", "components.UserList.validationEmail": "Email required", + "components.UserList.validationPhoneNumber": "Invalid phone number", "components.UserList.validationUsername": "You must provide an username", "components.UserList.validationpasswordminchars": "Password is too short; should be a minimum of 8 characters", "components.UserProfile.ProfileHeader.joindate": "Joined {joindate}", diff --git a/src/styles/globals.css b/src/styles/globals.css index 00793a42bb..c51e66a641 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -316,6 +316,7 @@ input[type='text'], input[type='password'], + input[type='tel'], select, textarea { @apply block w-full min-w-0 flex-1 rounded-md border border-gray-500 bg-gray-700 text-white transition duration-150 ease-in-out sm:text-sm sm:leading-5; From 70f4a9504c1e14e73077d2d2eb7f7b362d9dd57c Mon Sep 17 00:00:00 2001 From: Modar Abo Hassoun Date: Mon, 6 Jul 2026 19:12:04 +0200 Subject: [PATCH 3/7] feat(user): add phone number field in user settings --- .../interfaces/api/userSettingsInterfaces.ts | 1 + server/routes/user/usersettings.ts | 4 ++++ src/components/UserList/index.tsx | 3 +-- .../UserGeneralSettings/index.tsx | 22 +++++++++++++++++++ src/i18n/locale/de.json | 2 ++ src/i18n/locale/en.json | 2 ++ src/utils/numberHelpers.ts | 2 ++ 7 files changed, 34 insertions(+), 2 deletions(-) diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index 57e7b3f614..6f055c2330 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -3,6 +3,7 @@ import type { NotificationAgentKey } from '@server/lib/settings'; export interface UserSettingsGeneralResponse { username?: string; email?: string; + phoneNumber?: string; locale?: string; discoverRegion?: string; streamingRegion?: string; diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index bff500d58a..181a326226 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -49,6 +49,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>( return res.status(200).json({ username: user.username, email: user.email, + phoneNumber: user.phoneNumber, locale: user.settings?.locale, discoverRegion: user.settings?.discoverRegion, streamingRegion: user.settings?.streamingRegion, @@ -108,6 +109,8 @@ userSettingsRoutes.post< throw new ApiError(400, ApiErrorCode.InvalidEmail); } + user.phoneNumber = req.body.phoneNumber; + // Update quota values only if the user has the correct permissions if ( !user.hasPermission(Permission.MANAGE_USERS) && @@ -149,6 +152,7 @@ userSettingsRoutes.post< watchlistSyncMovies: savedUser.settings?.watchlistSyncMovies, watchlistSyncTv: savedUser.settings?.watchlistSyncTv, email: savedUser.email, + phoneNumber: savedUser.phoneNumber, }); } catch (e) { if (e.errorCode) { diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index e7323067f7..70f7af1a37 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -17,6 +17,7 @@ import type { User } from '@app/hooks/useUser'; import { Permission, UserType, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { PHONE_REGEX } from '@app/utils/numberHelpers'; import { Transition } from '@headlessui/react'; import { BarsArrowDownIcon, @@ -43,8 +44,6 @@ import validator from 'validator'; import * as Yup from 'yup'; import JellyfinImportModal from './JellyfinImportModal'; -const PHONE_REGEX = /^\+?[0-9]{1,15}$/; - const messages = defineMessages('components.UserList', { users: 'Users', userlist: 'User List', diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index c0e9100869..473cb1e890 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -13,6 +13,7 @@ import { Permission, UserType, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import ErrorPage from '@app/pages/_error'; import defineMessages from '@app/utils/defineMessages'; +import { PHONE_REGEX } from '@app/utils/numberHelpers'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ApiErrorCode } from '@server/constants/error'; import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces'; @@ -33,6 +34,7 @@ const messages = defineMessages( generalsettings: 'General Settings', displayName: 'Display Name', email: 'Email', + phoneNumber: 'Phone number', save: 'Save Changes', saving: 'Saving…', mediaServerUser: '{mediaServerName} User', @@ -63,6 +65,7 @@ const messages = defineMessages( languageDefault: 'Default ({language})', validationemailrequired: 'Email required', validationemailformat: 'Valid email required', + validationPhoneNumber: 'Invalid phone number', plexwatchlistsyncmovies: 'Auto-Request Movies', plexwatchlistsyncmoviestip: 'Automatically request movies on your Plex Watchlist', @@ -115,6 +118,10 @@ const UserGeneralSettings = () => { (value) => !value || validator.isEmail(value, { require_tld: false }) ), + phoneNumber: Yup.string().matches(PHONE_REGEX, { + message: intl.formatMessage(messages.validationPhoneNumber), + excludeEmptyString: true, + }), }); useEffect(() => { @@ -151,6 +158,7 @@ const UserGeneralSettings = () => { initialValues={{ displayName: data?.username !== user?.email ? data?.username : '', email: data?.email?.includes('@') ? data.email : '', + phoneNumber: data?.phoneNumber, locale: data?.locale, discoverRegion: data?.discoverRegion, streamingRegion: data?.streamingRegion, @@ -170,6 +178,7 @@ const UserGeneralSettings = () => { username: values.displayName, email: values.email || user?.jellyfinUsername || user?.plexUsername, + phoneNumber: values.phoneNumber, locale: values.locale, discoverRegion: values.discoverRegion, streamingRegion: values.streamingRegion, @@ -332,6 +341,19 @@ const UserGeneralSettings = () => { )} +
+ +
+
+ +
+ {errors.phoneNumber && touched.phoneNumber && ( +
{errors.phoneNumber}
+ )} +
+