diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 3631e25..09be2b0 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -51,6 +51,14 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } config: { usesNonExemptEncryption: false, }, + infoPlist: { + // Allow connecting to self-hosted Open WebUI servers that are reachable + // only over plain HTTP (local network, Docker, Raspberry Pi, Tailscale). + NSAppTransportSecurity: { + NSAllowsArbitraryLoads: true, + NSAllowsLocalNetworking: true, + }, + }, }, android: { package: appId, @@ -107,6 +115,16 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } enableBackgroundRecording: false, }, ], + [ + 'expo-build-properties', + { + android: { + // Allow plain HTTP connections to self-hosted servers (local network, + // Tailscale, etc.). Mirrors iOS NSAllowsArbitraryLoads above. + usesCleartextTraffic: true, + }, + }, + ], ]), newArchEnabled: true, extra, diff --git a/i18n/mobile/shared/en.json b/i18n/mobile/shared/en.json index 8221ea9..596f871 100644 --- a/i18n/mobile/shared/en.json +++ b/i18n/mobile/shared/en.json @@ -4,7 +4,7 @@ "TEXT_INVALID_EMAIL": "Please enter a valid email address", "TEXT_REQUIRED_EMAIL": "Please enter your email", "TEXT_REQUIRED_PASSWORD": "Please enter your password", - "TEXT_INVALID_URL": "Please enter a valid URL in format https://your.domain", + "TEXT_INVALID_URL": "Please enter a valid server URL, e.g. https://your.domain or http://192.168.1.10:3000", "TEXT_REQUIRED_FOLDER_NAME": "Please enter a folder name" }, "COMMON": { diff --git a/libs/mobile/auth/features/email-sign-in-form/src/lib/forms/email-form.ts b/libs/mobile/auth/features/email-sign-in-form/src/lib/forms/email-form.ts index d02467d..e86e80f 100644 --- a/libs/mobile/auth/features/email-sign-in-form/src/lib/forms/email-form.ts +++ b/libs/mobile/auth/features/email-sign-in-form/src/lib/forms/email-form.ts @@ -1,6 +1,6 @@ import { i18n } from '@ronas-it/react-native-common-modules/i18n'; import * as Yup from 'yup'; -import { emailValidator } from '@open-webui-react-native/mobile/shared/utils/validation'; +import { emailValidator, serverUrlValidator } from '@open-webui-react-native/mobile/shared/utils/validation'; export class EmailFormSchema { public email: string; @@ -17,21 +17,7 @@ export class EmailFormSchema { return Yup.object().shape({ email: emailValidator().required(i18n.t('SHARED.VALIDATION.TEXT_REQUIRED_EMAIL')), password: Yup.string().required(i18n.t('SHARED.VALIDATION.TEXT_REQUIRED_PASSWORD')), - url: Yup.string() - .url(i18n.t('SHARED.VALIDATION.TEXT_INVALID_URL')) - .test('no-path', i18n.t('SHARED.VALIDATION.TEXT_INVALID_URL'), (value) => { - if (!value) return true; - - try { - const normalizedValue = value.trim(); - const url = new URL(normalizedValue); - const normalizedPath = url.pathname.replace(/\/+$/, '/'); - - return normalizedPath === '/' && !url.search && !url.hash; - } catch { - return false; - } - }), + url: serverUrlValidator(), }); } } diff --git a/libs/mobile/shared/utils/validation/src/index.ts b/libs/mobile/shared/utils/validation/src/index.ts index 7edbb65..e2cc97a 100644 --- a/libs/mobile/shared/utils/validation/src/index.ts +++ b/libs/mobile/shared/utils/validation/src/index.ts @@ -1 +1,2 @@ export * from './email-validation'; +export * from './server-url-validation'; diff --git a/libs/mobile/shared/utils/validation/src/server-url-validation.ts b/libs/mobile/shared/utils/validation/src/server-url-validation.ts new file mode 100644 index 0000000..b501187 --- /dev/null +++ b/libs/mobile/shared/utils/validation/src/server-url-validation.ts @@ -0,0 +1,29 @@ +import { i18n } from '@ronas-it/react-native-common-modules/i18n'; +import * as Yup from 'yup'; + +export const serverUrlValidator = (): Yup.StringSchema => + Yup.string() + .trim() + .test('server-url', i18n.t('SHARED.VALIDATION.TEXT_INVALID_URL'), (value) => { + if (!value) { + return true; + } + + try { + const url = new URL(value.trim()); + + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + return false; + } + + if (!url.hostname) { + return false; + } + + const normalizedPath = url.pathname.replace(/\/+$/, '/'); + + return normalizedPath === '/' && !url.search && !url.hash; + } catch { + return false; + } + });