diff --git a/app.config.ts b/app.config.ts index 336550d1..7a4c5151 100644 --- a/app.config.ts +++ b/app.config.ts @@ -14,9 +14,18 @@ import sqlitePlugin from "expo-sqlite/plugin"; import statusBarPlugin from "expo-status-bar/plugin"; import withProtectUserDataPlugin from "./plugins/withProtectUserData/plugin"; -const IS_DEV_BUILD = process.env.APP_VARIANT === "development"; -const IS_PREVIEW_BUILD = process.env.APP_VARIANT === "preview"; -const IS_PRODUCTION_BUILD = process.env.APP_VARIANT === "production"; +const ENV_APP_VARIANT = process.env["APP_VARIANT"]; +const ENV_ANDROID_ABIS = process.env["ANDROID_ABIS"]; + +/** + * --------------------------------------------------------------------------- + * Types + * --------------------------------------------------------------------------- + */ + +type BuildType = "development" | "preview" | "production"; + +type APK_ABIS = "armeabi-v7a" | "arm64-v8a" | "x86" | "x86_64"; type AdaptiveLauncherAppIcon = { backgroundColor: string; @@ -25,7 +34,24 @@ type AdaptiveLauncherAppIcon = { monochromeImage: string; }; -type APK_ABIS = "armeabi-v7a" | "arm64-v8a" | "x86" | "x86_64"; +type BuildConfig = { + type: BuildType; + + nameSuffix: string; + packageNameSuffix: string; + versionNameSuffix: string; + + icon: string; + adaptiveIcon: AdaptiveLauncherAppIcon | undefined; + + androidAbis: APK_ABIS[]; +}; + +/** + * --------------------------------------------------------------------------- + * Build constants + * --------------------------------------------------------------------------- + */ const UNIVERSAL_ABIS: APK_ABIS[] = [ "armeabi-v7a", @@ -34,88 +60,161 @@ const UNIVERSAL_ABIS: APK_ABIS[] = [ "x86_64", ]; -let appNameSuffix = ""; -let packageNameSuffix = ""; -let versionNameSuffix = ""; -let launcherAppIcon = "./assets/images/passcodes-icon.png"; -let adaptiveLauncherAppIcon: AdaptiveLauncherAppIcon | undefined = { +const DEFAULT_NON_PRODUCTION_ABIS: APK_ABIS[] = ["arm64-v8a", "x86_64"]; + +const DEFAULT_ICON = "./assets/images/passcodes-icon.png"; + +const ADAPTIVE_ICON: AdaptiveLauncherAppIcon = { backgroundColor: "#34597f", foregroundImage: "./assets/images/android-icon-launcher-foreground.png", backgroundImage: "./assets/images/android-icon-launcher-background.png", monochromeImage: "./assets/images/android-icon-launcher-monochrome.png", }; -let buildAPKABIS: APK_ABIS[] = process.env.ANDROID_ABIS - ? (process.env.ANDROID_ABIS.split(",") as APK_ABIS[]) - : UNIVERSAL_ABIS; - -if (IS_DEV_BUILD) { - appNameSuffix = " Dev"; - packageNameSuffix = ".dev"; - versionNameSuffix = "-Dev"; - launcherAppIcon = "./assets/images/passcodes-dev-icon.png"; -} else if (IS_PREVIEW_BUILD) { - appNameSuffix = " Preview"; - packageNameSuffix = ".preview"; - versionNameSuffix = "-Preview"; - launcherAppIcon = "./assets/images/passcodes-preview-icon.png"; -} - -if (!IS_PRODUCTION_BUILD) { - adaptiveLauncherAppIcon = undefined; - buildAPKABIS = process.env.ANDROID_ABIS - ? (process.env.ANDROID_ABIS.split(",") as APK_ABIS[]) - : ["arm64-v8a", "x86_64"]; -} +/** + * --------------------------------------------------------------------------- + * Build type & varaints + * --------------------------------------------------------------------------- + * Missing or invalid APP_VARIANT intentionally means production. + */ +const BUILD_TYPE: BuildType = + ENV_APP_VARIANT === "development" || + ENV_APP_VARIANT === "preview" || + ENV_APP_VARIANT === "production" + ? ENV_APP_VARIANT + : "production"; + +const BUILD_VARAIANTS: APK_ABIS[] | undefined = ENV_ANDROID_ABIS + ? (ENV_ANDROID_ABIS.split(",") as APK_ABIS[]) + : undefined; + +/** + * --------------------------------------------------------------------------- + * Build configuration + * --------------------------------------------------------------------------- + * This is the single place that describes how each build variant differs. + */ +const BUILD_CONFIGS: Record = { + development: { + type: "development", + + nameSuffix: " Dev", + packageNameSuffix: ".dev", + versionNameSuffix: "-Dev", + + icon: "./assets/images/passcodes-dev-icon.png", + adaptiveIcon: undefined, + + androidAbis: BUILD_VARAIANTS + ? BUILD_VARAIANTS + : DEFAULT_NON_PRODUCTION_ABIS, + }, + + preview: { + type: "preview", + + nameSuffix: " Preview", + packageNameSuffix: ".preview", + versionNameSuffix: "-Preview", + + icon: "./assets/images/passcodes-preview-icon.png", + adaptiveIcon: undefined, + + androidAbis: BUILD_VARAIANTS + ? BUILD_VARAIANTS + : DEFAULT_NON_PRODUCTION_ABIS, + }, + + production: { + type: "production", + + nameSuffix: "", + packageNameSuffix: "", + versionNameSuffix: "", + + icon: DEFAULT_ICON, + adaptiveIcon: ADAPTIVE_ICON, + + androidAbis: BUILD_VARAIANTS ? BUILD_VARAIANTS : UNIVERSAL_ABIS, + }, +}; + +const build = BUILD_CONFIGS[BUILD_TYPE]; + +/** + * --------------------------------------------------------------------------- + * Application identity + * --------------------------------------------------------------------------- + */ +const APP_IDENTITY = { + packageName: "com.jeeldobariya.passcodes", + iosBundleIdentifier: "com.jeeldobariya.passcodes.earlybeta", + + version: AppConfig.BUILD_VERSION, + androidVersionCode: AppConfig.BUILD_ANDROID_VERSIONCODE, + iosBuildNumber: AppConfig.BUILD_IOS_VERSIONCODE, + + slug: "passcodes", +}; + +/** + * --------------------------------------------------------------------------- + * Expo configuration + * --------------------------------------------------------------------------- + */ export default ({ config }: ConfigContext): ExpoConfig => ({ ...config, - name: AppConfig.APP_NAME + appNameSuffix, - slug: "passcodes", - version: AppConfig.BUILD_VERSION + versionNameSuffix, + + name: `${AppConfig.APP_NAME}${build.nameSuffix}`, + slug: APP_IDENTITY.slug, + version: `${APP_IDENTITY.version}${build.versionNameSuffix}`, orientation: "default", - icon: launcherAppIcon, + icon: build.icon, scheme: "passcodes", userInterfaceStyle: "automatic", + githubUrl: "https://github.com/PasscodesApp/Passcodes", platforms: ["android", "ios"], ios: { - buildNumber: AppConfig.BUILD_IOS_VERSIONCODE, - version: AppConfig.BUILD_VERSION, - icon: launcherAppIcon, - bundleIdentifier: - "com.jeeldobariya.passcodes.earlybeta" + packageNameSuffix, + buildNumber: APP_IDENTITY.iosBuildNumber, + version: APP_IDENTITY.version, + icon: build.icon, + + bundleIdentifier: `${APP_IDENTITY.iosBundleIdentifier}${build.packageNameSuffix}`, + supportsTablet: true, + infoPlist: { ITSAppUsesNonExemptEncryption: false, }, }, android: { - versionCode: AppConfig.BUILD_ANDROID_VERSIONCODE, - version: AppConfig.BUILD_VERSION, - package: "com.jeeldobariya.passcodes" + packageNameSuffix, - icon: launcherAppIcon, - adaptiveIcon: { - backgroundColor: adaptiveLauncherAppIcon?.backgroundColor, - foregroundImage: adaptiveLauncherAppIcon?.foregroundImage, - backgroundImage: adaptiveLauncherAppIcon?.backgroundImage, - monochromeImage: adaptiveLauncherAppIcon?.monochromeImage, - }, + versionCode: APP_IDENTITY.androidVersionCode, + version: APP_IDENTITY.version, + + package: `${APP_IDENTITY.packageName}${build.packageNameSuffix}`, + + icon: build.icon, + + adaptiveIcon: build.adaptiveIcon ?? {}, + predictiveBackGestureEnabled: false, }, web: { output: "static", - favicon: launcherAppIcon, + favicon: build.icon, }, plugins: [ routerPlugin(), + splashScreenPlugin({ - image: launcherAppIcon, + image: build.icon, imageWidth: 200, resizeMode: "contain", backgroundColor: "#7eabee", @@ -123,42 +222,53 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ backgroundColor: "#34597f", }, }), + devBuildPlugin({ launchMode: "launcher", addGeneratedScheme: false, }), + buildPropertiesPlugin({ android: { usePrecompiledHeaders: true, enableMinifyInReleaseBuilds: true, enableShrinkResourcesInReleaseBuilds: true, - - // NOTE: buildArchs: APK_ABIS[] = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"], - buildArchs: buildAPKABIS, + buildArchs: build.androidAbis, }, }), + localAuthenticationPlugin({ faceIDPermission: "Allow Passcodes to use Face ID.", }), + statusBarPlugin({ style: "dark" }), + sqlitePlugin(), sharingPlugin(), + expoScreenOrientationPlugin({ initialOrientation: "PORTRAIT", }), - withProtectUserDataPlugin({ allowBackup: false }), // TODO: we make it to allow backup when we have encryption. + + withProtectUserDataPlugin({ + allowBackup: false, // TODO: we make it to allow backup when we have encryption. + }), "@react-native-vector-icons/fontawesome6", + "react-native-quick-crypto", ], + experiments: { typedRoutes: true, reactCompiler: true, }, + extra: { router: {}, eas: { projectId: "960e68d8-0220-4068-8f1f-e141d29dbcef", }, }, + owner: "passcodesapp", }); diff --git a/package-lock.json b/package-lock.json index f0756023..274b6899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "react-dom": "19.2.3", "react-native": "0.86.3", "react-native-paper": "^5.15.3", + "react-native-quick-crypto": "^1.1.7", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "~4.26.0", "react-native-web": "~0.21.0" @@ -1170,6 +1171,30 @@ "node": ">=0.10.0" } }, + "node_modules/@craftzdog/react-native-buffer": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@craftzdog/react-native-buffer/-/react-native-buffer-6.1.2.tgz", + "integrity": "sha512-KV1HitN05FHLLDG7Zb/yftDsa+mKBYBzFMQ0PMldvUicq6vWOtAvz9mDavt7Fzozh+WNqORE+yFDkkdWysZ/SA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "ieee754": "^1.2.1", + "react-native-quick-base64": "^3.0.0" + } + }, "node_modules/@drizzle-team/brocli": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", @@ -3347,8 +3372,8 @@ } }, "node_modules/@passcodes/passalgo": { - "version": "0.1.0", - "resolved": "git+ssh://git@github.com/PasscodesApp/PassAlgo.git#0b19e9c7324bc49fc189ba362588badf4c350546", + "version": "0.2.0", + "resolved": "git+ssh://git@github.com/PasscodesApp/PassAlgo.git#00477efa9a1adca189f04e5d6d35a4d020108694", "license": "MIT" }, "node_modules/@radix-ui/primitive": { @@ -5255,7 +5280,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -5632,6 +5656,30 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5651,7 +5699,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5670,7 +5717,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5684,7 +5730,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6189,7 +6234,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -6429,7 +6473,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6568,7 +6611,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6615,7 +6657,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7213,6 +7254,15 @@ "node": ">=6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expo": { "version": "57.0.22", "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.22.tgz", @@ -8319,7 +8369,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -8422,7 +8471,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8450,7 +8498,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8484,7 +8531,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -8634,7 +8680,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8675,7 +8720,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -8704,7 +8748,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8717,7 +8760,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -8847,6 +8889,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8957,6 +9019,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9061,7 +9139,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9190,7 +9267,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.4", @@ -9275,7 +9351,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -9358,7 +9433,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -10155,7 +10229,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11369,7 +11442,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11460,6 +11532,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -11759,6 +11840,17 @@ "react-native": "*" } }, + "node_modules/react-native-nitro-modules": { + "version": "0.37.1", + "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.37.1.tgz", + "integrity": "sha512-KpW6EQVQ/bfegpCGxN9Be+ndvsfj56t4IESEvCASu9gWpJa/NDfHpIeIwCOvZQ3eXmLEj6YD4wCLqcC1FJPr2w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-paper": { "version": "5.15.3", "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.15.3.tgz", @@ -11804,6 +11896,45 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, + "node_modules/react-native-quick-base64": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-native-quick-base64/-/react-native-quick-base64-3.0.1.tgz", + "integrity": "sha512-EjUP2U7WqKmlMmoY7XGyHomy8bM0q4+yCDCRg4ZezQ6zedYRwc7yVk4V2O/iSftKaLEzhuW98lpyPMdk1iPHXQ==", + "license": "MIT", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-quick-crypto": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/react-native-quick-crypto/-/react-native-quick-crypto-1.1.7.tgz", + "integrity": "sha512-4On/q3uENJNk6uCyxtigRQzAxPS/h75nDtljX2MBdGwELkz2p0O/eNFSsYIV8l2vYXxyyva6jXQHBCxAcryZ9A==", + "license": "MIT", + "dependencies": { + "@craftzdog/react-native-buffer": "^6.1.2", + "events": "3.3.0", + "readable-stream": "4.7.0", + "safe-buffer": "^5.2.1", + "string_decoder": "^1.3.0", + "util": "0.12.5" + }, + "peerDependencies": { + "expo": ">=48.0.0", + "react": "*", + "react-native": "*", + "react-native-nitro-modules": ">=0.31.2", + "react-native-quick-base64": ">=3.0.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, "node_modules/react-native-reanimated": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.3.tgz", @@ -12047,6 +12178,22 @@ } } }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -12288,7 +12435,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -12418,7 +12564,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -12775,6 +12920,15 @@ "node": ">=4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -14001,6 +14155,19 @@ } } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -14308,7 +14475,6 @@ "version": "1.1.22", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", diff --git a/package.json b/package.json index 97354ab3..923d1efa 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "react-dom": "19.2.3", "react-native": "0.86.3", "react-native-paper": "^5.15.3", + "react-native-quick-crypto": "^1.1.7", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "~4.26.0", "react-native-web": "~0.21.0" diff --git a/src/app/(tabs)/password-generator.tsx b/src/app/(tabs)/password-generator.tsx index 6608fd68..8c13ffd1 100644 --- a/src/app/(tabs)/password-generator.tsx +++ b/src/app/(tabs)/password-generator.tsx @@ -1,419 +1,10 @@ -import FormTextField from "@/components/FormTextField"; -import Text from "@/components/Text"; -import { - DEFAULT_PASSWORD_OPTIONS, - generatePassword, - getPasswordStrength, - MAX_PASSWORD_LENGTH, - MIN_PASSWORD_LENGTH, - type CharacterType, - type PasswordGeneratorOptions, -} from "@/utils/generating"; - -import Slider from "@react-native-community/slider"; -import FontAwesome6 from "@react-native-vector-icons/fontawesome6"; -import * as Clipboard from "expo-clipboard"; -import { useEffect, useState } from "react"; -import { ScrollView, StyleSheet, View } from "react-native"; -import { Button, Card, Divider, useTheme } from "react-native-paper"; +import PasswordGeneratorScreenContent from "@/features/password-generator/ScreenContent"; import { SafeAreaView } from "react-native-safe-area-context"; -const CHARACTER_OPTIONS: { - key: CharacterType; - label: string; - example: string; -}[] = [ - { - key: "uppercase", - label: "Uppercase", - example: "ABC", - }, - { - key: "lowercase", - label: "Lowercase", - example: "abc", - }, - { - key: "numbers", - label: "Numbers", - example: "123", - }, - { - key: "symbols", - label: "Symbols", - example: "!@#", - }, -]; - export default function PasswordGeneratorScreen() { - const theme = useTheme(); - - const [options, setOptions] = useState( - DEFAULT_PASSWORD_OPTIONS, - ); - - const [password, setPassword] = useState(() => - generatePassword(DEFAULT_PASSWORD_OPTIONS), - ); - - const strength = getPasswordStrength(options); - - /* - * Regenerate the password whenever any generator - * option changes. - */ - useEffect(() => { - setPassword(generatePassword(options)); - }, [options]); - - function updateOption(key: CharacterType) { - setOptions((current) => { - const enabledCharacterTypes = CHARACTER_OPTIONS.filter( - (option) => current[option.key], - ); - - /* - * Keep at least one character type enabled. - */ - if (current[key] && enabledCharacterTypes.length === 1) { - return current; - } - - return { - ...current, - [key]: !current[key], - }; - }); - } - - function updateLength(length: number) { - setOptions((current) => ({ - ...current, - length: Math.round(length), - })); - } - - function generate() { - setPassword(generatePassword(options)); - } - - async function copyPassword() { - if (!password) { - return; - } - - await Clipboard.setStringAsync(password); - } - return ( - - - - Password Generator - - - {/* Generated Password */} - - - - - Generated Password - - - - - - - - - - - - - {/* Password Length */} - - - - - - Length - - - Choose the length of your password - - - - - {options.length} - - - - - - - {MIN_PASSWORD_LENGTH} - - {MAX_PASSWORD_LENGTH} - - - - - {/* Character Types */} - - - - - Character Types - - - Choose which characters can be used - - - - - {CHARACTER_OPTIONS.map((option, index) => ( - - updateOption(option.key)} - /> - - {index < CHARACTER_OPTIONS.length - 1 && } - - ))} - - - - - {/* Password Strength */} - - - - - - Password Strength - - - Based on length and character variety - - - - - {strength.label} - - - - - - - + + ); } - -type GeneratorOptionProps = { - label: string; - example: string; - enabled: boolean; - onPress: () => void; -}; - -function GeneratorOption({ - label, - example, - enabled, - onPress, -}: GeneratorOptionProps) { - return ( - - ); -} - -function StrengthIndicator({ score }: { score: number }) { - return ( - - - {Array.from({ length: 5 }).map((_, index) => ( - - ))} - - - - Weak - Strong - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingVertical: 12, - }, - - content: { - paddingHorizontal: 12, - paddingBottom: 96, - gap: 24, - }, - - title: { - marginTop: 8, - marginBottom: 4, - marginHorizontal: 4, - textAlign: "center", - }, - - cardContent: { - padding: 16, - gap: 16, - }, - - sectionLabel: { - opacity: 0.7, - }, - - actionContainer: { - flexDirection: "row-reverse", - gap: 8, - }, - - sectionHeader: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - gap: 16, - }, - - headerText: { - flex: 1, - }, - - description: { - opacity: 0.6, - marginTop: 3, - }, - - lengthValue: { - minWidth: 40, - textAlign: "center", - }, - - rangeLabels: { - flexDirection: "row", - justifyContent: "space-between", - opacity: 0.6, - }, - - optionList: { - marginHorizontal: -8, - }, - - optionButton: { - marginHorizontal: 0, - }, - - optionButtonContent: { - justifyContent: "flex-start", - }, - - strengthContainer: { - gap: 8, - }, - - strengthBars: { - flexDirection: "row", - gap: 5, - }, - - strengthBar: { - flex: 1, - height: 7, - borderRadius: 4, - backgroundColor: "#ddd", - }, - - strengthBarActive: { - backgroundColor: "#236636", - }, - - strengthLabels: { - flexDirection: "row", - justifyContent: "space-between", - opacity: 0.6, - }, -}); diff --git a/src/features/password-generator/GeneratorOption.component.tsx b/src/features/password-generator/GeneratorOption.component.tsx new file mode 100644 index 00000000..d7a60de6 --- /dev/null +++ b/src/features/password-generator/GeneratorOption.component.tsx @@ -0,0 +1,49 @@ +import Text from "@/components/Text"; +import FontAwesome6 from "@react-native-vector-icons/fontawesome6"; +import { StyleSheet } from "react-native"; +import { Button } from "react-native-paper"; + +type GeneratorOptionProps = { + label: string; + example: string; + enabled: boolean; + onPress: () => void; +}; + +export default function GeneratorOption({ + label, + example, + enabled, + onPress, +}: GeneratorOptionProps) { + return ( + + ); +} + +const styles = StyleSheet.create({ + optionButton: { + marginHorizontal: 0, + }, + + optionButtonContent: { + justifyContent: "flex-start", + }, +}); diff --git a/src/features/password-generator/ScreenContent.tsx b/src/features/password-generator/ScreenContent.tsx new file mode 100644 index 00000000..a81e2af8 --- /dev/null +++ b/src/features/password-generator/ScreenContent.tsx @@ -0,0 +1,323 @@ +import FormTextField from "@/components/FormTextField"; +import Text from "@/components/Text"; +import StrengthIndicator from "@/features/password-generator/StrengthIndicator.component"; +import { + type CharacterType, + generatePassword, + getPasswordStrength, + type PasswordGeneratorOptions, +} from "@passcodes/passalgo/passwords"; +import Slider from "@react-native-community/slider"; +import FontAwesome6 from "@react-native-vector-icons/fontawesome6"; +import * as Clipboard from "expo-clipboard"; +import { useEffect, useState } from "react"; +import { ScrollView, StyleSheet, View } from "react-native"; +import { Button, Card, Divider, useTheme } from "react-native-paper"; +import GeneratorOption from "./GeneratorOption.component"; +import { + DEFAULT_PASSWORD_OPTIONS, + MAX_PASSWORD_LENGTH, + MIN_PASSWORD_LENGTH, + quickCryptoRandom, +} from "./utils"; + +const CHARACTER_OPTIONS: { + key: CharacterType; + label: string; + example: string; +}[] = [ + { + key: "uppercase", + label: "Uppercase", + example: "ABC", + }, + { + key: "lowercase", + label: "Lowercase", + example: "abc", + }, + { + key: "numbers", + label: "Numbers", + example: "123", + }, + { + key: "symbols", + label: "Symbols", + example: "!@#", + }, +]; + +export default function PasswordGeneratorScreenContent() { + const theme = useTheme(); + + const [options, setOptions] = useState( + DEFAULT_PASSWORD_OPTIONS, + ); + + const [password, setPassword] = useState(() => + generatePassword(options, quickCryptoRandom), + ); + + const strength = getPasswordStrength(password); + + /* + * Regenerate the password whenever any generator + * option changes. + */ + useEffect(() => { + setPassword(generatePassword(options, quickCryptoRandom)); + }, [options]); + + function updateOption(key: CharacterType) { + setOptions((current) => { + const enabledCharacterTypes = CHARACTER_OPTIONS.filter( + (option) => current[option.key], + ); + + /* + * Keep at least one character type enabled. + */ + if (current[key] && enabledCharacterTypes.length === 1) { + return current; + } + + return { + ...current, + [key]: !current[key], + }; + }); + } + + function updateLength(length: number) { + setOptions((current) => ({ + ...current, + length: Math.round(length), + })); + } + + function generate() { + setPassword(generatePassword(options, quickCryptoRandom)); + } + + async function copyPassword() { + if (!password) { + return; + } + + await Clipboard.setStringAsync(password); + } + + return ( + + + Password Generator + + + {/* Generated Password */} + + + + + Generated Password + + + + + + + + + + + + + {/* Password Length */} + + + + + + Length + + + Choose the length of your password + + + + + {options.length} + + + + + + + {MIN_PASSWORD_LENGTH} + + {MAX_PASSWORD_LENGTH} + + + + + {/* Character Types */} + + + + + Character Types + + + Choose which characters can be used + + + + + {CHARACTER_OPTIONS.map((option, index) => ( + + updateOption(option.key)} + /> + + {index < CHARACTER_OPTIONS.length - 1 && } + + ))} + + + + + {/* Password Strength */} + + + + + + Password Strength + + + Based on length and character variety + + + + + {strength.label} + + + + + + + + ); +} + +const styles = StyleSheet.create({ + content: { + paddingHorizontal: 12, + paddingBottom: 96, + gap: 24, + }, + + title: { + marginTop: 8, + marginBottom: 4, + marginHorizontal: 4, + textAlign: "center", + }, + + cardContent: { + padding: 16, + gap: 16, + }, + + sectionLabel: { + opacity: 0.7, + }, + + actionContainer: { + flexDirection: "row-reverse", + gap: 8, + }, + + sectionHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + gap: 16, + }, + + headerText: { + flex: 1, + }, + + description: { + opacity: 0.6, + marginTop: 3, + }, + + lengthValue: { + minWidth: 40, + textAlign: "center", + }, + + rangeLabels: { + flexDirection: "row", + justifyContent: "space-between", + opacity: 0.6, + }, + + optionList: { + marginHorizontal: -8, + }, +}); diff --git a/src/features/password-generator/StrengthIndicator.component.tsx b/src/features/password-generator/StrengthIndicator.component.tsx new file mode 100644 index 00000000..0eaeef9a --- /dev/null +++ b/src/features/password-generator/StrengthIndicator.component.tsx @@ -0,0 +1,91 @@ +import Text from "@/components/Text"; +import { StyleSheet, View } from "react-native"; + +type StrengthIndicatorProps = { + /** + * Normalized password strength score. + * + * Must be a value between 0 and 1: + * - 0 = no strength + * - 1 = maximum strength + */ + score: number; + + /** + * Number of bars used to visually represent the strength. + * + * Defaults to 5. + */ + bars?: number; +}; + +/** + * Displays a password strength score as a series of bars. + * + * The `score` is expected to be normalized to the range 0..1. + * The score is mapped proportionally to the configured number + * of bars. For example, with 5 bars: + * + * 0 → 0 active bars + * 0.2 → 1 active bar + * 0.5 → 3 active bars + * 1 → 5 active bars + * + * A score between 0 and 1 is rounded up so that any non-zero + * strength is represented by at least one active bar. + */ +export default function StrengthIndicator({ + score, + bars = 5, +}: StrengthIndicatorProps) { + const activeBars = Math.ceil(score * bars); + + return ( + + + {Array.from({ length: bars }).map((_, index) => ( + + ))} + + + + Weak + Strong + + + ); +} + +const styles = StyleSheet.create({ + strengthContainer: { + gap: 8, + }, + + strengthBars: { + flexDirection: "row", + gap: 5, + }, + + strengthBar: { + flex: 1, + height: 7, + borderRadius: 4, + backgroundColor: "#ddd", + }, + + strengthBarActive: { + backgroundColor: "#236636", + }, + + strengthLabels: { + flexDirection: "row", + justifyContent: "space-between", + opacity: 0.6, + }, +}); diff --git a/src/features/password-generator/utils.ts b/src/features/password-generator/utils.ts new file mode 100644 index 00000000..28435a3f --- /dev/null +++ b/src/features/password-generator/utils.ts @@ -0,0 +1,22 @@ +import type { + PasswordGeneratorOptions, + RandomSource, +} from "@passcodes/passalgo/passwords"; +import { randomInt } from "react-native-quick-crypto"; + +export const quickCryptoRandom: RandomSource = { + randomInt(max) { + return randomInt(0, max); + }, +}; + +export const DEFAULT_PASSWORD_OPTIONS: PasswordGeneratorOptions = { + length: 16, + uppercase: true, + lowercase: true, + numbers: false, + symbols: false, +}; + +export const MIN_PASSWORD_LENGTH = 4; +export const MAX_PASSWORD_LENGTH = 64; diff --git a/src/utils/generating.ts b/src/utils/generating.ts deleted file mode 100644 index 4cd1135e..00000000 --- a/src/utils/generating.ts +++ /dev/null @@ -1,130 +0,0 @@ -export const MIN_PASSWORD_LENGTH = 8; -export const MAX_PASSWORD_LENGTH = 64; - -const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; -const LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; -const NUMBERS = "0123456789"; -const SYMBOLS = "!@#$%^&*()-_=+[]{};:,.<>?"; - -export type PasswordGeneratorOptions = { - length: number; - uppercase: boolean; - lowercase: boolean; - numbers: boolean; - symbols: boolean; -}; - -export type PasswordStrength = { - score: number; - label: "Weak" | "Medium" | "Strong"; -}; - -export type CharacterType = keyof Omit; - -function randomCharacter(characters: string): string { - return characters[Math.floor(Math.random() * characters.length)]; -} - -export function generatePassword(options: PasswordGeneratorOptions): string { - const characterSets: string[] = []; - - if (options.uppercase) { - characterSets.push(UPPERCASE); - } - - if (options.lowercase) { - characterSets.push(LOWERCASE); - } - - if (options.numbers) { - characterSets.push(NUMBERS); - } - - if (options.symbols) { - characterSets.push(SYMBOLS); - } - - if (characterSets.length === 0) { - return ""; - } - - const allCharacters = characterSets.join(""); - - const characters: string[] = []; - - // Guarantee that every selected character type - // appears at least once. - for (const characterSet of characterSets) { - characters.push(randomCharacter(characterSet)); - } - - while (characters.length < options.length) { - characters.push(randomCharacter(allCharacters)); - } - - // Fisher-Yates shuffle. - for (let i = characters.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - - [characters[i], characters[j]] = [characters[j], characters[i]]; - } - - return characters.join(""); -} - -export function getPasswordStrength( - options: PasswordGeneratorOptions, -): PasswordStrength { - let score = 0; - - if (options.length >= 12) { - score++; - } - - if (options.length >= 20) { - score++; - } - - if (options.uppercase) { - score++; - } - - if (options.lowercase) { - score++; - } - - if (options.numbers) { - score++; - } - - if (options.symbols) { - score++; - } - - if (score <= 2) { - return { - score, - label: "Weak", - }; - } - - if (score <= 4) { - return { - score, - label: "Medium", - }; - } - - return { - score, - label: "Strong", - }; -} - -export const DEFAULT_PASSWORD_OPTIONS: PasswordGeneratorOptions = { - length: 20, - uppercase: true, - lowercase: true, - numbers: true, - symbols: true, -};