Compare commits

...

4 Commits

Author SHA1 Message Date
renovate[bot]
6d48f22f82 chore(deps): Update CI dependencies to v3.1.0 2026-06-10 22:54:45 +00:00
Gauvain
0a2dadffd2 feat(settings): graduated version tracking (build, branch, commit) (#1677)
Some checks are pending
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Waiting to run
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Waiting to run
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Waiting to run
🏗️ Build Apps / 🍎 Build iOS IPA (Phone - Unsigned) (push) Waiting to run
🏗️ Build Apps / 🍎 Build tvOS IPA (push) Waiting to run
🏗️ Build Apps / 🍎 Build tvOS IPA (Unsigned) (push) Waiting to run
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Waiting to run
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Waiting to run
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Waiting to run
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Waiting to run
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Waiting to run
🛡️ Trivy Security Scan / 🔎 Filesystem scan (push) Waiting to run
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Waiting to run
🚦 Security & Quality Gate / 🔍 Lint & Test (i18n:check) (push) Waiting to run
Co-authored-by: retardgerman <78982850+retardgerman@users.noreply.github.com>
2026-06-10 17:19:28 +02:00
Gauvain
6818ea380f fix(renovate): resolve maven lookups, unnest config, gate Expo SDK updates (#1708)
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone - Unsigned) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build tvOS IPA (push) Has been cancelled
🏗️ Build Apps / 🍎 Build tvOS IPA (Unsigned) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (i18n:check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🛡️ Trivy Security Scan / 🔎 Filesystem scan (push) Has been cancelled
2026-06-10 11:22:49 +02:00
lance chant
7cf0a13317 fix: an issue with save account didn't show the modal (#1705)
Co-authored-by: Gauvain <contact@uruk.dev>
Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-06-10 10:28:18 +02:00
8 changed files with 187 additions and 28 deletions

54
.github/renovate.json vendored
View File

@@ -44,22 +44,42 @@
] ]
} }
}, },
"lockFileMaintenance": { "vulnerabilityAlerts": {
"vulnerabilityAlerts": { "enabled": true,
"enabled": true, "addLabels": ["security", "vulnerability"],
"addLabels": ["security", "vulnerability"], "assigneesFromCodeOwners": true,
"assigneesFromCodeOwners": true, "commitMessageSuffix": " [SECURITY]"
"commitMessageSuffix": " [SECURITY]" },
"packageRules": [
{
"description": "Expo SDK coherence: expo, react, react-native and Expo-managed modules are pinned by the Expo SDK and must move together (via `expo install --fix`), so do not raise individual update PRs — group them and require manual approval from the Dependency Dashboard",
"matchPackageNames": [
"expo",
"react",
"react-dom",
"react-native",
"react-native-web",
"expo-*",
"@expo/*"
],
"groupName": "Expo SDK",
"dependencyDashboardApproval": true
}, },
"packageRules": [ {
{ "description": "Group minor and patch GitHub Action updates into a single PR",
"description": "Group minor and patch GitHub Action updates into a single PR", "matchManagers": ["github-actions"],
"matchManagers": ["github-actions"], "groupName": "CI dependencies",
"groupName": "CI dependencies", "groupSlug": "ci-deps",
"groupSlug": "ci-deps", "matchUpdateTypes": ["minor", "patch", "digest", "pin"],
"matchUpdateTypes": ["minor", "patch", "digest", "pin"], "automerge": true
"automerge": true },
} {
] "description": "androidx and other Google-hosted Maven packages resolve from Google's Maven repository (not Maven Central)",
} "matchDatasources": ["maven"],
"registryUrls": [
"https://dl.google.com/dl/android/maven2/",
"https://repo.maven.apache.org/maven2/"
]
}
]
} }

View File

@@ -11,6 +11,12 @@ on:
push: push:
branches: [develop, master] branches: [develop, master]
# Exposed to `expo prebuild` (app.config.js → extra.build) so Settings can show the
# branch + commit a CI build was made from. EAS cloud builds use EAS_BUILD_* instead.
env:
EXPO_PUBLIC_GIT_BRANCH: ${{ github.head_ref || github.ref_name }}
EXPO_PUBLIC_GIT_COMMIT: ${{ github.sha }}
jobs: jobs:
build-android-phone: build-android-phone:
if: (!contains(github.event.head_commit.message, '[skip ci]')) if: (!contains(github.event.head_commit.message, '[skip ci]'))

View File

@@ -17,7 +17,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: 🚩 Apply merge conflict label - name: 🚩 Apply merge conflict label
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
with: with:
dirtyLabel: '⚔️ merge-conflict' dirtyLabel: '⚔️ merge-conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'

View File

@@ -1,3 +1,41 @@
const { execFileSync } = require("node:child_process");
// Build metadata, injected into `extra.build` and read at runtime via
// expo-constants (see utils/version.ts). Sources in priority order:
// EAS cloud build → GitHub Actions → explicit EXPO_PUBLIC_* → local git → null.
const git = (args) => {
try {
return execFileSync("git", args, { stdio: ["ignore", "pipe", "ignore"] })
.toString()
.trim();
} catch {
return null;
}
};
const buildMeta = {
commit:
(
process.env.EAS_BUILD_GIT_COMMIT_HASH ||
process.env.GITHUB_SHA ||
process.env.EXPO_PUBLIC_GIT_COMMIT ||
git(["rev-parse", "HEAD"]) ||
""
).slice(0, 7) || null,
branch:
process.env.EAS_BUILD_GIT_BRANCH ||
process.env.GITHUB_HEAD_REF ||
process.env.GITHUB_REF_NAME ||
process.env.EXPO_PUBLIC_GIT_BRANCH ||
git(["rev-parse", "--abbrev-ref", "HEAD"]) ||
null,
profile:
process.env.EAS_BUILD_PROFILE ||
process.env.EXPO_PUBLIC_BUILD_PROFILE ||
null,
builtAt: new Date().toISOString(),
};
module.exports = ({ config }) => { module.exports = ({ config }) => {
if (process.env.EXPO_TV !== "1") { if (process.env.EXPO_TV !== "1") {
config.plugins.push("expo-background-task"); config.plugins.push("expo-background-task");
@@ -22,6 +60,8 @@ module.exports = ({ config }) => {
androidConfig.googleServicesFile = process.env.GOOGLE_SERVICES_JSON; androidConfig.googleServicesFile = process.env.GOOGLE_SERVICES_JSON;
} }
config.extra = { ...config.extra, build: buildMeta };
return { return {
...(Object.keys(androidConfig).length > 0 && { android: androidConfig }), ...(Object.keys(androidConfig).length > 0 && { android: androidConfig }),
...config, ...config,

View File

@@ -69,17 +69,23 @@ export const SaveAccountModal: React.FC<SaveAccountModalProps> = ({
[isAndroid], [isAndroid],
); );
const isPresentedRef = useRef(false);
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
bottomSheetModalRef.current?.present(); bottomSheetModalRef.current?.present();
} else { } else if (isPresentedRef.current) {
bottomSheetModalRef.current?.dismiss(); bottomSheetModalRef.current?.dismiss();
isPresentedRef.current = false;
} }
}, [visible]); }, [visible]);
const handleSheetChanges = useCallback( const handleSheetChanges = useCallback(
(index: number) => { (index: number) => {
if (index === -1) { if (index >= 0) {
isPresentedRef.current = true;
} else if (index === -1 && isPresentedRef.current) {
isPresentedRef.current = false;
resetState(); resetState();
onClose(); onClose();
} }

View File

@@ -1,8 +1,8 @@
import * as Application from "expo-application";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getVersionInfo } from "@/utils/version";
import { ListGroup } from "../list/ListGroup"; import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
@@ -13,10 +13,9 @@ export const UserInfo: React.FC<Props> = ({ ...props }) => {
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const { t } = useTranslation(); const { t } = useTranslation();
const version = // Graduated build identifier — see utils/version.ts:
Application?.nativeApplicationVersion || // dev → "0.54.1 · branch · commit", develop/CI → "0.54.1 · commit", production → "0.54.1 (42)".
Application?.nativeBuildVersion || const { display: version } = getVersionInfo();
"N/A";
return ( return (
<View {...props}> <View {...props}>

View File

@@ -40,6 +40,7 @@ import {
} from "@/utils/secureCredentials"; } from "@/utils/secureCredentials";
import { store } from "@/utils/store"; import { store } from "@/utils/store";
import { clearTVDiscoverySafely } from "@/utils/tvDiscovery/sync"; import { clearTVDiscoverySafely } from "@/utils/tvDiscovery/sync";
import { APP_VERSION } from "@/utils/version";
interface Server { interface Server {
address: string; address: string;
@@ -53,7 +54,7 @@ const initialApi = (() => {
const id = getOrSetDeviceId(); const id = getOrSetDeviceId();
const deviceName = getDeviceNameSync(); const deviceName = getDeviceNameSync();
const jellyfinInstance = new Jellyfin({ const jellyfinInstance = new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.54.1" }, clientInfo: { name: "Streamyfin", version: APP_VERSION },
deviceInfo: { deviceInfo: {
name: deviceName, name: deviceName,
id, id,
@@ -135,7 +136,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const id = getOrSetDeviceId(); const id = getOrSetDeviceId();
const deviceName = getDeviceNameSync(); const deviceName = getDeviceNameSync();
return new Jellyfin({ return new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.54.1" }, clientInfo: { name: "Streamyfin", version: APP_VERSION },
deviceInfo: { deviceInfo: {
name: deviceName, name: deviceName,
id, id,
@@ -169,7 +170,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
return { return {
authorization: `MediaBrowser Client="Streamyfin", Device=${ authorization: `MediaBrowser Client="Streamyfin", Device=${
Platform.OS === "android" ? "Android" : "iOS" Platform.OS === "android" ? "Android" : "iOS"
}, DeviceId="${deviceId}", Version="0.54.1"`, }, DeviceId="${deviceId}", Version="${APP_VERSION}"`,
}; };
}, [deviceId]); }, [deviceId]);

87
utils/version.ts Normal file
View File

@@ -0,0 +1,87 @@
import * as Application from "expo-application";
import Constants from "expo-constants";
/** Raw marketing version (app.json `version`), e.g. "0.54.1". Exposed so the Jellyfin
* clientInfo auto-tracks the app version instead of a hardcoded string. */
export const APP_VERSION = Application.nativeApplicationVersion ?? "unknown";
/** Build metadata injected at build time by `app.config.js` into `extra.build`. */
export interface BuildMeta {
commit?: string | null;
branch?: string | null;
profile?: string | null;
builtAt?: string | null;
}
export interface VersionInfo {
/** Marketing version (CFBundleShortVersionString / android versionName), e.g. "0.54.1". */
version: string | null;
/** Build number (CFBundleVersion / versionCode), e.g. "42". */
build: string | null;
/** Short git commit the build was made from, e.g. "a1b2c3d". */
commit: string | null;
/** Git branch the build was made from, e.g. "develop". */
branch: string | null;
/** EAS build profile, e.g. "production", "preview", or null for local. */
profile: string | null;
isDev: boolean;
isProduction: boolean;
/** Graduated label for the Settings "App version" row (see tiering below). */
display: string;
}
/**
* Resolve a graduated version string for Settings.
*
* Tiering (most → least detailed):
* - dev / local build → `version · branch · commit` (full context for debugging)
* - develop / CI / preview → `version · commit` (pin the exact source)
* - production (store / TestFlight) → `version (build)` (store-correlatable; the
* build number lets TestFlight reports pin a build whose version isn't a
* published release. Note: TestFlight and the public App Store ship the same
* binary — telling them apart needs a runtime iOS receipt check, intentionally
* not done here.)
*/
export function getVersionInfo(): VersionInfo {
// Read native/config values defensively — a version string must never crash Settings
// (e.g. a dev build whose native expo-constants is out of sync with the JS).
const read = <T>(fn: () => T): T | null => {
try {
return fn() ?? null;
} catch {
return null;
}
};
const version = read(() => Application.nativeApplicationVersion);
const build = read(() => Application.nativeBuildVersion);
const meta = (read(() => Constants.expoConfig?.extra?.build) ??
{}) as BuildMeta;
const commit = meta.commit ?? null;
const branch = meta.branch ?? null;
const profile = meta.profile ?? null;
const isDev = __DEV__ === true;
const isProduction =
typeof profile === "string" && profile.startsWith("production");
let display: string;
if (isDev) {
display = [version ?? "dev", branch, commit].filter(Boolean).join(" · ");
} else if (isProduction) {
display =
version && build ? `${version} (${build})` : (version ?? build ?? "N/A");
} else {
display = [version, commit].filter(Boolean).join(" · ") || version || "N/A";
}
return {
version,
build,
commit,
branch,
profile,
isDev,
isProduction,
display,
};
}