Automatisation des screenshots
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 328 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 274 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
|
|
@ -16,8 +16,10 @@ const appPath = resolve(
|
|||
"Release-iphonesimulator",
|
||||
"mobile.app",
|
||||
);
|
||||
const productsPath = resolve(derivedDataPath, "Build", "Products", "Release-iphonesimulator");
|
||||
const defaultOutputRoot = resolve(mobileRoot, "..", "ScreenShots");
|
||||
const bundleId = "fr.negopoly.app";
|
||||
const screenshotStorageKey = "negopoly:screenshot-scene";
|
||||
const launchWaitMs = 4500;
|
||||
const sceneWaitMs = 2200;
|
||||
|
||||
|
|
@ -159,19 +161,78 @@ function buildReleaseApp(buildDestinationUdid) {
|
|||
{ stdio: "inherit" },
|
||||
);
|
||||
|
||||
if (!existsSync(appPath)) {
|
||||
throw new Error(`Built app not found at ${appPath}`);
|
||||
return resolveBuiltAppPath();
|
||||
}
|
||||
|
||||
function resolveBuiltAppPath() {
|
||||
if (existsSync(appPath)) {
|
||||
return appPath;
|
||||
}
|
||||
|
||||
if (!existsSync(productsPath)) {
|
||||
throw new Error(`Build products directory not found at ${productsPath}`);
|
||||
}
|
||||
|
||||
const candidates = readdirSync(productsPath)
|
||||
.filter((entry) => entry.endsWith(".app"))
|
||||
.filter((entry) => !entry.endsWith(".app.dSYM"))
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
|
||||
if (candidates.length === 0) {
|
||||
throw new Error(`Built app not found under ${productsPath}`);
|
||||
}
|
||||
|
||||
return join(productsPath, candidates[0]);
|
||||
}
|
||||
|
||||
function installApp(udid, builtAppPath) {
|
||||
runQuiet("xcrun", ["simctl", "terminate", udid, bundleId]);
|
||||
runQuiet("xcrun", ["simctl", "uninstall", udid, bundleId]);
|
||||
run("xcrun", ["simctl", "install", udid, builtAppPath]);
|
||||
}
|
||||
|
||||
function grantAppPermissions(udid) {
|
||||
runQuiet("xcrun", ["simctl", "privacy", udid, "grant", "notifications", bundleId]);
|
||||
}
|
||||
|
||||
function getDataContainerPath(udid) {
|
||||
return run("xcrun", ["simctl", "get_app_container", udid, bundleId, "data"]).trim();
|
||||
}
|
||||
|
||||
function writeScreenshotScene(udid, scene) {
|
||||
const containerPath = getDataContainerPath(udid);
|
||||
const storageDir = join(
|
||||
containerPath,
|
||||
"Library",
|
||||
"Application Support",
|
||||
bundleId,
|
||||
"RCTAsyncLocalStorage_V1",
|
||||
);
|
||||
const manifestPath = join(storageDir, "manifest.json");
|
||||
|
||||
mkdirSync(storageDir, { recursive: true });
|
||||
|
||||
let manifest = {};
|
||||
if (existsSync(manifestPath)) {
|
||||
try {
|
||||
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
||||
} catch {
|
||||
manifest = {};
|
||||
}
|
||||
}
|
||||
|
||||
function installApp(udid) {
|
||||
runQuiet("xcrun", ["simctl", "terminate", udid, bundleId]);
|
||||
runQuiet("xcrun", ["simctl", "uninstall", udid, bundleId]);
|
||||
run("xcrun", ["simctl", "install", udid, appPath]);
|
||||
manifest[screenshotStorageKey] = scene;
|
||||
writeFileSync(manifestPath, JSON.stringify(manifest), "utf8");
|
||||
}
|
||||
|
||||
function launchApp(udid) {
|
||||
run("xcrun", ["simctl", "launch", udid, bundleId]);
|
||||
}
|
||||
|
||||
function openScene(udid, scene, waitMs) {
|
||||
run("xcrun", ["simctl", "openurl", udid, `negopoly://screenshot/${scene}`]);
|
||||
runQuiet("xcrun", ["simctl", "terminate", udid, bundleId]);
|
||||
writeScreenshotScene(udid, scene);
|
||||
launchApp(udid);
|
||||
sleep(waitMs);
|
||||
}
|
||||
|
||||
|
|
@ -186,14 +247,16 @@ function main() {
|
|||
...target,
|
||||
simulator: resolveDevice(target.matchName, devices),
|
||||
}));
|
||||
const builtAppPath = !options.skipBuild
|
||||
? buildReleaseApp(resolvedTargets[0].simulator.udid)
|
||||
: resolveBuiltAppPath();
|
||||
|
||||
if (!options.skipBuild) {
|
||||
buildReleaseApp(resolvedTargets[0].simulator.udid);
|
||||
} else if (!existsSync(appPath)) {
|
||||
throw new Error(`Cannot use --skip-build because ${appPath} does not exist`);
|
||||
if (options.skipBuild && !existsSync(builtAppPath)) {
|
||||
throw new Error(`Cannot use --skip-build because ${builtAppPath} does not exist`);
|
||||
}
|
||||
|
||||
mkdirSync(options.outputRoot, { recursive: true });
|
||||
console.log(`Using built app: ${builtAppPath}`);
|
||||
|
||||
for (const target of resolvedTargets) {
|
||||
const { simulator } = target;
|
||||
|
|
@ -203,7 +266,8 @@ function main() {
|
|||
mkdirSync(deviceOutputDir, { recursive: true });
|
||||
ensureBooted(simulator.udid);
|
||||
setSimulatorAppearance(simulator.udid);
|
||||
installApp(simulator.udid);
|
||||
installApp(simulator.udid, builtAppPath);
|
||||
grantAppPermissions(simulator.udid);
|
||||
|
||||
for (const [index, scene] of scenes.entries()) {
|
||||
const targetPath = join(deviceOutputDir, scene.fileName);
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ function RootNavigationGate() {
|
|||
const lastTargetRef = useRef<keyof RootStackParamList | null>(null);
|
||||
const lastLinkRef = useRef<string | null>(null);
|
||||
const pendingNotificationRef = useRef<NotificationTarget | null>(null);
|
||||
const pendingScreenshotRef = useRef(false);
|
||||
const lastScreenshotSceneRef = useRef<string | null>(null);
|
||||
const lastNotificationIdRef = useRef<string | null>(null);
|
||||
const theme = useTheme();
|
||||
const navigationTheme = getNavigationTheme(theme);
|
||||
|
|
@ -72,7 +72,6 @@ function RootNavigationGate() {
|
|||
(url: string) => {
|
||||
const scene = extractScreenshotScene(url);
|
||||
if (!scene) return false;
|
||||
pendingScreenshotRef.current = true;
|
||||
manager.activateScreenshotScene(scene);
|
||||
return true;
|
||||
},
|
||||
|
|
@ -150,12 +149,15 @@ function RootNavigationGate() {
|
|||
}, [manager.isBanker, manager.session, manager.sessionId, navReady, navigationRef]);
|
||||
|
||||
const processPendingScreenshot = useCallback(() => {
|
||||
if (!pendingScreenshotRef.current) return;
|
||||
const screenshot = manager.screenshot;
|
||||
if (!screenshot) {
|
||||
lastScreenshotSceneRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (lastScreenshotSceneRef.current === screenshot.scene) return;
|
||||
if (!navReady || !navigationRef.isReady()) return;
|
||||
const target = manager.screenshot?.navigationTarget;
|
||||
if (!target) return;
|
||||
const target = screenshot.navigationTarget;
|
||||
|
||||
pendingScreenshotRef.current = false;
|
||||
navigationRef.dispatch(
|
||||
CommonActions.reset({
|
||||
index: 0,
|
||||
|
|
@ -163,10 +165,24 @@ function RootNavigationGate() {
|
|||
{
|
||||
name: target.root,
|
||||
params: target.params,
|
||||
state: target.state,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const followUp = target.followUp;
|
||||
if (followUp) {
|
||||
setTimeout(() => {
|
||||
if (!navigationRef.isReady()) return;
|
||||
navigationRef.dispatch(
|
||||
CommonActions.navigate({
|
||||
name: followUp.name,
|
||||
params: followUp.params,
|
||||
}),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
lastScreenshotSceneRef.current = screenshot.scene;
|
||||
lastTargetRef.current = target.root;
|
||||
}, [manager.screenshot, navReady, navigationRef]);
|
||||
|
||||
|
|
@ -195,9 +211,7 @@ function RootNavigationGate() {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingScreenshotRef.current) {
|
||||
return;
|
||||
}
|
||||
if (manager.screenshot) return;
|
||||
if (!navReady || !navigationRef.isReady()) return;
|
||||
|
||||
const currentRoute = navigationRef.getCurrentRoute();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@ import type { SessionSnapshot } from "../shared/types";
|
|||
|
||||
export type ScreenshotScene = "start" | "lobby" | "home" | "transfers" | "chat";
|
||||
|
||||
export type ScreenshotNavigationState = {
|
||||
index: number;
|
||||
routes: Array<{
|
||||
name: string;
|
||||
params?: Record<string, unknown>;
|
||||
state?: ScreenshotNavigationState;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ScreenshotFixture = {
|
||||
scene: ScreenshotScene;
|
||||
sessionId: string;
|
||||
|
|
@ -11,6 +20,11 @@ export type ScreenshotFixture = {
|
|||
navigationTarget: {
|
||||
root: "EntryLanding" | "Lobby" | "PlayerTabs";
|
||||
params?: Record<string, unknown>;
|
||||
state?: ScreenshotNavigationState;
|
||||
followUp?: {
|
||||
name: "PlayerTabs";
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
transferDraft?: {
|
||||
targetId: string;
|
||||
|
|
@ -300,8 +314,15 @@ export function buildScreenshotFixture(scene: ScreenshotScene): ScreenshotFixtur
|
|||
session: createActiveSession(),
|
||||
navigationTarget: {
|
||||
root: "PlayerTabs",
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [{ name: "PlayerHome" }],
|
||||
},
|
||||
followUp: {
|
||||
name: "PlayerTabs",
|
||||
params: { screen: "PlayerHome" },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -314,8 +335,15 @@ export function buildScreenshotFixture(scene: ScreenshotScene): ScreenshotFixtur
|
|||
session: createActiveSession(),
|
||||
navigationTarget: {
|
||||
root: "PlayerTabs",
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [{ name: "PlayerTransfers" }],
|
||||
},
|
||||
followUp: {
|
||||
name: "PlayerTabs",
|
||||
params: { screen: "PlayerTransfers" },
|
||||
},
|
||||
},
|
||||
transferDraft: {
|
||||
targetId: malikId,
|
||||
amount: "90",
|
||||
|
|
@ -332,6 +360,26 @@ export function buildScreenshotFixture(scene: ScreenshotScene): ScreenshotFixtur
|
|||
session: createActiveSession(),
|
||||
navigationTarget: {
|
||||
root: "PlayerTabs",
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: "PlayerChat",
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: "ChatList" },
|
||||
{
|
||||
name: "ChatThread",
|
||||
params: { chatId: "group-direct-malik" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
followUp: {
|
||||
name: "PlayerTabs",
|
||||
params: {
|
||||
screen: "PlayerChat",
|
||||
params: {
|
||||
|
|
@ -340,5 +388,6 @@ export function buildScreenshotFixture(scene: ScreenshotScene): ScreenshotFixtur
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AppState, type AppStateStatus } from "react-native";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import type { JoinResponse, SessionPreview, SessionSnapshot } from "../shared/types";
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from "./connection";
|
||||
|
||||
const STORAGE_KEY = "negopoly:session";
|
||||
const SCREENSHOT_STORAGE_KEY = "negopoly:screenshot-scene";
|
||||
|
||||
type StoredSession = {
|
||||
sessionId: string;
|
||||
|
|
@ -51,12 +52,34 @@ async function clearStoredSession() {
|
|||
await AsyncStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
async function readStoredScreenshotScene(): Promise<ScreenshotScene | null> {
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem(SCREENSHOT_STORAGE_KEY);
|
||||
return raw === "start" ||
|
||||
raw === "lobby" ||
|
||||
raw === "home" ||
|
||||
raw === "transfers" ||
|
||||
raw === "chat"
|
||||
? raw
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function clearStoredScreenshotScene() {
|
||||
await AsyncStorage.removeItem(SCREENSHOT_STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function useSessionManager() {
|
||||
const [sessionId, setSessionId] = useState("");
|
||||
const [sessionCode, setSessionCode] = useState("");
|
||||
const [playerId, setPlayerId] = useState("");
|
||||
const [session, setSession] = useState<SessionSnapshot | null>(null);
|
||||
const [screenshot, setScreenshot] = useState<ScreenshotFixture | null>(null);
|
||||
const [bootstrapReady, setBootstrapReady] = useState(false);
|
||||
const [pushRegistrationReady, setPushRegistrationReady] = useState(false);
|
||||
const [screenshotBootstrapPending, setScreenshotBootstrapPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [connectionState, setConnectionState] =
|
||||
useState<SessionConnectionState>("idle");
|
||||
|
|
@ -318,25 +341,59 @@ export function useSessionManager() {
|
|||
screenshotRef.current = screenshot;
|
||||
}, [screenshot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (screenshot) {
|
||||
setScreenshotBootstrapPending(false);
|
||||
}
|
||||
}, [screenshot]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionRef.current = session;
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
readStoredSession().then((stored) => {
|
||||
(async () => {
|
||||
let enablePushRegistration = true;
|
||||
try {
|
||||
const screenshotScene = await readStoredScreenshotScene();
|
||||
if (!mounted) return;
|
||||
if (screenshotScene) {
|
||||
enablePushRegistration = false;
|
||||
setScreenshotBootstrapPending(true);
|
||||
await clearStoredScreenshotScene();
|
||||
if (!mounted) return;
|
||||
activateScreenshotScene(screenshotScene);
|
||||
return;
|
||||
}
|
||||
const stored = await readStoredSession();
|
||||
if (!mounted || !stored || screenshotRef.current) return;
|
||||
setSessionId(stored.sessionId);
|
||||
setSessionCode(stored.sessionCode);
|
||||
setPlayerId(stored.playerId);
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setPushRegistrationReady(enablePushRegistration);
|
||||
setBootstrapReady(true);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (screenshot) return;
|
||||
if (
|
||||
!bootstrapReady ||
|
||||
!pushRegistrationReady ||
|
||||
screenshot ||
|
||||
screenshotBootstrapPending ||
|
||||
!sessionId ||
|
||||
!playerId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let mounted = true;
|
||||
registerForPushNotificationsAsync().then((token) => {
|
||||
if (!mounted) return;
|
||||
|
|
@ -345,7 +402,14 @@ export function useSessionManager() {
|
|||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [screenshot]);
|
||||
}, [
|
||||
bootstrapReady,
|
||||
playerId,
|
||||
pushRegistrationReady,
|
||||
screenshot,
|
||||
screenshotBootstrapPending,
|
||||
sessionId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (screenshot) return;
|
||||
|
|
@ -423,9 +487,10 @@ export function useSessionManager() {
|
|||
};
|
||||
}, [playerId, screenshot, sessionId]);
|
||||
|
||||
const activateScreenshotScene = useCallback((scene: ScreenshotScene) => {
|
||||
function activateScreenshotScene(scene: ScreenshotScene) {
|
||||
const fixture = buildScreenshotFixture(scene);
|
||||
suppressReconnectRef.current = true;
|
||||
setPushRegistrationReady(false);
|
||||
teardownConnection();
|
||||
reconnectAttemptRef.current = 0;
|
||||
setReconnectAttempt(0);
|
||||
|
|
@ -438,7 +503,7 @@ export function useSessionManager() {
|
|||
setSessionCode(fixture.sessionCode);
|
||||
setPlayerId(fixture.playerId);
|
||||
setSession(fixture.session);
|
||||
}, []);
|
||||
}
|
||||
|
||||
async function registerPushTokenFor(targetSessionId: string, targetPlayerId: string) {
|
||||
if (!pushToken) return;
|
||||
|
|
|
|||