Compare commits

...

2 commits

Author SHA1 Message Date
52aff4242d
fix: revert broken subscriptions 2026-01-12 22:05:19 +01:00
906e2f194d fix(ios): bundle release version 2026-01-12 18:30:06 +01:00
4 changed files with 155 additions and 190 deletions

View file

@ -16,6 +16,9 @@
"bundle:ios:export": "./scripts/ios-export.sh", "bundle:ios:export": "./scripts/ios-export.sh",
"bundle:ios:upload": "./scripts/ios-upload.sh", "bundle:ios:upload": "./scripts/ios-upload.sh",
"bundle:ios": "yarn bundle:ios:archive && yarn bundle:ios:export", "bundle:ios": "yarn bundle:ios:archive && yarn bundle:ios:export",
"bundle:ios:testflight:version": "bash ./scripts/ios-set-testflight-build-number.sh",
"bundle:ios:testflight:archive": "yarn bundle:ios:testflight:version && yarn bundle:ios:archive",
"bundle:ios:testflight": "yarn bundle:ios:testflight:archive && yarn bundle:ios:export && yarn bundle:ios:upload",
"ios": "expo run:ios", "ios": "expo run:ios",
"ios:staging": "dotenv --override -e .env.staging -- yarn run ios", "ios:staging": "dotenv --override -e .env.staging -- yarn run ios",
"ios:prod": "dotenv --override -e .env.prod -- yarn run ios", "ios:prod": "dotenv --override -e .env.prod -- yarn run ios",

View file

@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
INFO_PLIST_PATH="$PROJECT_ROOT_DIR/ios/AlerteSecours/Info.plist"
if [ ! -f "$INFO_PLIST_PATH" ]; then
echo "error: Info.plist not found at $INFO_PLIST_PATH" >&2
exit 1
fi
# Generate a unique version/build number for TestFlight in Europe/Paris timezone
# This value will be used for both CFBundleShortVersionString and CFBundleVersion.
# Format: YYYYMMDDHHMM, e.g. 202601121110 (valid as a numeric-only iOS version).
BUILD_NUMBER="$(TZ=Europe/Paris date +%Y%m%d%H%M)"
echo "[ios-set-testflight-build-number] Using build number: $BUILD_NUMBER"
PLISTBUDDY="/usr/libexec/PlistBuddy"
if [ ! -x "$PLISTBUDDY" ]; then
echo "error: $PLISTBUDDY not found or not executable. This script must run on macOS with PlistBuddy available." >&2
exit 1
fi
set_plist_version_key() {
local key="$1"
# Try to set the existing key, or add it if it does not exist yet.
if ! "$PLISTBUDDY" -c "Set :$key $BUILD_NUMBER" "$INFO_PLIST_PATH" 2>/dev/null; then
"$PLISTBUDDY" -c "Add :$key string $BUILD_NUMBER" "$INFO_PLIST_PATH"
fi
}
set_plist_version_key "CFBundleVersion"
set_plist_version_key "CFBundleShortVersionString"
echo "[ios-set-testflight-build-number] Updated CFBundleShortVersionString and CFBundleVersion in $INFO_PLIST_PATH to $BUILD_NUMBER"
# Print the build number on stdout so callers can capture/log it easily.
echo "$BUILD_NUMBER"

View file

@ -2,15 +2,8 @@ import { useRef, useEffect, useMemo, useState } from "react";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import * as Sentry from "@sentry/react-native"; import * as Sentry from "@sentry/react-native";
import { useNetworkState } from "~/stores"; import { useNetworkState } from "~/stores";
import { createLogger } from "~/lib/logger";
import { UI_SCOPES } from "~/lib/logger/scopes";
import useShallowMemo from "./useShallowMemo"; import useShallowMemo from "./useShallowMemo";
const hookLogger = createLogger({
module: UI_SCOPES.HOOKS,
feature: "useLatestWithSubscription",
});
// Constants for retry configuration // Constants for retry configuration
const MAX_RETRIES = 5; const MAX_RETRIES = 5;
const INITIAL_BACKOFF_MS = 1000; // 1 second const INITIAL_BACKOFF_MS = 1000; // 1 second
@ -54,15 +47,13 @@ export default function useLatestWithSubscription(
const retryCountRef = useRef(0); const retryCountRef = useRef(0);
const subscriptionErrorRef = useRef(null); const subscriptionErrorRef = useRef(null);
const timeoutIdRef = useRef(null); const timeoutIdRef = useRef(null);
const unsubscribeRef = useRef(null);
const lastWsClosedDateRef = useRef(null);
useEffect(() => { useEffect(() => {
const currentVarsHash = JSON.stringify(variables); const currentVarsHash = JSON.stringify(variables);
if (currentVarsHash !== variableHashRef.current) { if (currentVarsHash !== variableHashRef.current) {
hookLogger.debug("Variables changed; resetting subscription setup", { console.log(
subscriptionKey, `[${subscriptionKey}] Variables changed, resetting subscription setup`,
}); );
highestIdRef.current = null; highestIdRef.current = null;
variableHashRef.current = currentVarsHash; variableHashRef.current = currentVarsHash;
initialSetupDoneRef.current = false; initialSetupDoneRef.current = false;
@ -107,19 +98,19 @@ export default function useLatestWithSubscription(
(highestIdRef.current === null || highestId > highestIdRef.current) (highestIdRef.current === null || highestId > highestIdRef.current)
) { ) {
highestIdRef.current = highestId; highestIdRef.current = highestId;
hookLogger.debug("Updated subscription cursor to highest ID", { console.log(
subscriptionKey, `[${subscriptionKey}] Updated subscription cursor to highest ID:`,
highestId, highestId,
}); );
} }
} else { } else {
// Handle empty results case - initialize with 0 to allow subscription for first item // Handle empty results case - initialize with 0 to allow subscription for first item
if (highestIdRef.current === null) { if (highestIdRef.current === null) {
highestIdRef.current = 0; highestIdRef.current = 0;
hookLogger.debug("No initial items; setting subscription cursor", { console.log(
subscriptionKey, `[${subscriptionKey}] No initial items, setting subscription cursor to:`,
highestId: 0, 0,
}); );
} }
} }
}, [queryData, cursorKey, subscriptionKey]); }, [queryData, cursorKey, subscriptionKey]);
@ -143,20 +134,12 @@ export default function useLatestWithSubscription(
if (!subscribeToMore) return; if (!subscribeToMore) return;
if (highestIdRef.current === null) return; // Wait until we have the highest ID if (highestIdRef.current === null) return; // Wait until we have the highest ID
// Track WS close events so we only react when wsClosedDate actually changes
const wsClosedDateChanged =
!!wsClosedDate && wsClosedDate !== lastWsClosedDateRef.current;
if (wsClosedDateChanged) {
lastWsClosedDateRef.current = wsClosedDate;
}
// Check if max retries reached and we have an error // Check if max retries reached and we have an error
if (retryCountRef.current >= maxRetries && subscriptionErrorRef.current) { if (retryCountRef.current >= maxRetries && subscriptionErrorRef.current) {
hookLogger.error("Max retries reached; stopping subscription attempts", { console.error(
subscriptionKey, `[${subscriptionKey}] Max retries (${maxRetries}) reached. Stopping subscription attempts.`,
maxRetries, subscriptionErrorRef.current,
error: subscriptionErrorRef.current, );
});
// Report to Sentry when max retries are reached // Report to Sentry when max retries are reached
try { try {
@ -172,24 +155,17 @@ export default function useLatestWithSubscription(
}, },
}); });
} catch (sentryError) { } catch (sentryError) {
hookLogger.error("Failed to report max-retries to Sentry", { console.error("Failed to report to Sentry:", sentryError);
subscriptionKey,
error: sentryError,
});
} }
return; return;
} }
// Wait for: // Wait for:
// - initial setup not done yet // - either initial setup not done yet
// - OR a new wsClosedDate (WS reconnect) // - or a new wsClosedDate (WS reconnect)
// - OR a retry trigger // - or a retry trigger
if ( if (initialSetupDoneRef.current && !wsClosedDate && retryTrigger === 0) {
initialSetupDoneRef.current &&
!wsClosedDateChanged &&
retryTrigger === 0
) {
return; return;
} }
@ -202,16 +178,6 @@ export default function useLatestWithSubscription(
timeoutIdRef.current = null; timeoutIdRef.current = null;
} }
// Always cleanup any existing subscription before creating a new one
if (unsubscribeRef.current) {
try {
unsubscribeRef.current();
} catch (_error) {
// ignore
}
unsubscribeRef.current = null;
}
// Calculate backoff delay if this is a retry // Calculate backoff delay if this is a retry
const backoffDelay = const backoffDelay =
retryCountRef.current > 0 retryCountRef.current > 0
@ -221,13 +187,15 @@ export default function useLatestWithSubscription(
) )
: 0; : 0;
hookLogger.debug("Setting up subscription", { const retryMessage =
subscriptionKey, retryCountRef.current > 0
retryCount: retryCountRef.current, ? ` Retry attempt ${retryCountRef.current}/${maxRetries} after ${backoffDelay}ms delay`
maxRetries, : "";
backoffDelay,
highestId: highestIdRef.current, console.log(
}); `[${subscriptionKey}] Setting up subscription${retryMessage} with highestId:`,
highestIdRef.current,
);
// Use timeout for backoff // Use timeout for backoff
timeoutIdRef.current = setTimeout(() => { timeoutIdRef.current = setTimeout(() => {
@ -254,12 +222,10 @@ export default function useLatestWithSubscription(
maxRetries, maxRetries,
); );
hookLogger.warn("Subscription error", { console.error(
subscriptionKey, `[${subscriptionKey}] Subscription error (attempt ${retryCountRef.current}/${maxRetries}):`,
attempt: retryCountRef.current,
maxRetries,
error, error,
}); );
// If we haven't reached max retries, trigger a retry // If we haven't reached max retries, trigger a retry
if (retryCountRef.current < maxRetries) { if (retryCountRef.current < maxRetries) {
@ -304,11 +270,10 @@ export default function useLatestWithSubscription(
} }
}); });
hookLogger.debug("Received new items", { console.log(
subscriptionKey, `[${subscriptionKey}] Received ${filteredNewItems.length} new items, updated highestId:`,
receivedCount: filteredNewItems.length, highestIdRef.current,
highestId: highestIdRef.current, );
});
// For latest items pattern, we prepend new items (DESC order in UI) // For latest items pattern, we prepend new items (DESC order in UI)
return { return {
@ -318,27 +283,30 @@ export default function useLatestWithSubscription(
}, },
}); });
// Save unsubscribe for cleanup on reruns/unmount // Cleanup on unmount or re-run
unsubscribeRef.current = unsubscribe; return () => {
console.log(`[${subscriptionKey}] Cleaning up subscription`);
// Note: cleanup is handled by the effect cleanup below. if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
unsubscribe();
};
} catch (error) { } catch (error) {
// Handle setup errors (like malformed queries) // Handle setup errors (like malformed queries)
hookLogger.error("Error setting up subscription", { console.error(
subscriptionKey, `[${subscriptionKey}] Error setting up subscription:`,
error, error,
}); );
subscriptionErrorRef.current = error; subscriptionErrorRef.current = error;
// Increment retry counter but don't exceed maxRetries // Increment retry counter but don't exceed maxRetries
retryCountRef.current = Math.min(retryCountRef.current + 1, maxRetries); retryCountRef.current = Math.min(retryCountRef.current + 1, maxRetries);
hookLogger.warn("Subscription setup error", { console.error(
subscriptionKey, `[${subscriptionKey}] Subscription setup error (attempt ${retryCountRef.current}/${maxRetries}):`,
attempt: retryCountRef.current,
maxRetries,
error, error,
}); );
// If we haven't reached max retries, trigger a retry // If we haven't reached max retries, trigger a retry
if (retryCountRef.current < maxRetries) { if (retryCountRef.current < maxRetries) {
@ -360,14 +328,16 @@ export default function useLatestWithSubscription(
}, },
}); });
} catch (sentryError) { } catch (sentryError) {
hookLogger.error("Failed to report setup error to Sentry", { console.error("Failed to report to Sentry:", sentryError);
subscriptionKey,
error: sentryError,
});
} }
} }
// Cleanup is handled by the effect cleanup below. return () => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
};
} }
}, backoffDelay); }, backoffDelay);
@ -377,16 +347,6 @@ export default function useLatestWithSubscription(
clearTimeout(timeoutIdRef.current); clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null; timeoutIdRef.current = null;
} }
if (unsubscribeRef.current) {
try {
hookLogger.debug("Cleaning up subscription", { subscriptionKey });
unsubscribeRef.current();
} catch (_error) {
// ignore
}
unsubscribeRef.current = null;
}
}; };
}, [ }, [
skip, skip,

View file

@ -2,15 +2,8 @@ import { useRef, useEffect, useMemo, useState } from "react";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import * as Sentry from "@sentry/react-native"; import * as Sentry from "@sentry/react-native";
import { useNetworkState } from "~/stores"; import { useNetworkState } from "~/stores";
import { createLogger } from "~/lib/logger";
import { UI_SCOPES } from "~/lib/logger/scopes";
import useShallowMemo from "./useShallowMemo"; import useShallowMemo from "./useShallowMemo";
const hookLogger = createLogger({
module: UI_SCOPES.HOOKS,
feature: "useStreamQueryWithSubscription",
});
// Constants for retry configuration // Constants for retry configuration
const MAX_RETRIES = 5; const MAX_RETRIES = 5;
const INITIAL_BACKOFF_MS = 1000; // 1 second const INITIAL_BACKOFF_MS = 1000; // 1 second
@ -47,16 +40,14 @@ export default function useStreamQueryWithSubscription(
const retryCountRef = useRef(0); const retryCountRef = useRef(0);
const subscriptionErrorRef = useRef(null); const subscriptionErrorRef = useRef(null);
const timeoutIdRef = useRef(null); const timeoutIdRef = useRef(null);
const unsubscribeRef = useRef(null);
const lastWsClosedDateRef = useRef(null);
useEffect(() => { useEffect(() => {
const currentVarsHash = JSON.stringify(variables); const currentVarsHash = JSON.stringify(variables);
if (currentVarsHash !== variableHashRef.current) { if (currentVarsHash !== variableHashRef.current) {
hookLogger.debug("Variables changed; resetting cursor", { console.log(
subscriptionKey, `[${subscriptionKey}] Variables changed, resetting cursor to initial value:`,
initialCursor, initialCursor,
}); );
lastCursorRef.current = initialCursor; lastCursorRef.current = initialCursor;
variableHashRef.current = currentVarsHash; variableHashRef.current = currentVarsHash;
initialSetupDoneRef.current = false; initialSetupDoneRef.current = false;
@ -108,10 +99,10 @@ export default function useStreamQueryWithSubscription(
const newCursor = lastItem[cursorKey]; const newCursor = lastItem[cursorKey];
lastCursorRef.current = newCursor; lastCursorRef.current = newCursor;
hookLogger.debug("Updated subscription cursor", { console.log(
subscriptionKey, `[${subscriptionKey}] Updated subscription cursor:`,
cursor: newCursor, newCursor,
}); );
} }
}, [queryData, cursorKey, subscriptionKey]); }, [queryData, cursorKey, subscriptionKey]);
@ -133,20 +124,12 @@ export default function useStreamQueryWithSubscription(
if (skip) return; // If skipping, do nothing if (skip) return; // If skipping, do nothing
if (!subscribeToMore) return; if (!subscribeToMore) return;
// Track WS close events so we only react when wsClosedDate actually changes
const wsClosedDateChanged =
!!wsClosedDate && wsClosedDate !== lastWsClosedDateRef.current;
if (wsClosedDateChanged) {
lastWsClosedDateRef.current = wsClosedDate;
}
// Check if max retries reached and we have an error - this check must be done regardless of other conditions // Check if max retries reached and we have an error - this check must be done regardless of other conditions
if (retryCountRef.current >= maxRetries && subscriptionErrorRef.current) { if (retryCountRef.current >= maxRetries && subscriptionErrorRef.current) {
hookLogger.error("Max retries reached; stopping subscription attempts", { console.error(
subscriptionKey, `[${subscriptionKey}] Max retries (${maxRetries}) reached. Stopping subscription attempts.`,
maxRetries, subscriptionErrorRef.current,
error: subscriptionErrorRef.current, );
});
// Report to Sentry when max retries are reached // Report to Sentry when max retries are reached
try { try {
@ -162,24 +145,17 @@ export default function useStreamQueryWithSubscription(
}, },
}); });
} catch (sentryError) { } catch (sentryError) {
hookLogger.error("Failed to report max-retries to Sentry", { console.error("Failed to report to Sentry:", sentryError);
subscriptionKey,
error: sentryError,
});
} }
return; return;
} }
// Wait for: // Wait for:
// - initial setup not done yet // - either initial setup not done yet
// - OR a new wsClosedDate (WS reconnect) // - or a new wsClosedDate (WS reconnect)
// - OR a retry trigger // - or a retry trigger
if ( if (initialSetupDoneRef.current && !wsClosedDate && retryTrigger === 0) {
initialSetupDoneRef.current &&
!wsClosedDateChanged &&
retryTrigger === 0
) {
return; return;
} }
@ -192,16 +168,6 @@ export default function useStreamQueryWithSubscription(
timeoutIdRef.current = null; timeoutIdRef.current = null;
} }
// Always cleanup any existing subscription before creating a new one
if (unsubscribeRef.current) {
try {
unsubscribeRef.current();
} catch (_error) {
// ignore
}
unsubscribeRef.current = null;
}
// Calculate backoff delay if this is a retry // Calculate backoff delay if this is a retry
const backoffDelay = const backoffDelay =
retryCountRef.current > 0 retryCountRef.current > 0
@ -211,13 +177,15 @@ export default function useStreamQueryWithSubscription(
) )
: 0; : 0;
hookLogger.debug("Setting up subscription", { const retryMessage =
subscriptionKey, retryCountRef.current > 0
retryCount: retryCountRef.current, ? ` Retry attempt ${retryCountRef.current}/${maxRetries} after ${backoffDelay}ms delay`
maxRetries, : "";
backoffDelay,
cursor: lastCursorRef.current, console.log(
}); `[${subscriptionKey}] Setting up subscription${retryMessage} with cursor:`,
lastCursorRef.current,
);
// Use timeout for backoff // Use timeout for backoff
timeoutIdRef.current = setTimeout(() => { timeoutIdRef.current = setTimeout(() => {
@ -244,12 +212,10 @@ export default function useStreamQueryWithSubscription(
maxRetries, maxRetries,
); );
hookLogger.warn("Subscription error", { console.error(
subscriptionKey, `[${subscriptionKey}] Subscription error (attempt ${retryCountRef.current}/${maxRetries}):`,
attempt: retryCountRef.current,
maxRetries,
error, error,
}); );
// If we haven't reached max retries, trigger a retry // If we haven't reached max retries, trigger a retry
if (retryCountRef.current < maxRetries) { if (retryCountRef.current < maxRetries) {
@ -297,10 +263,10 @@ export default function useStreamQueryWithSubscription(
newItemCursor > lastCursorRef.current newItemCursor > lastCursorRef.current
) { ) {
lastCursorRef.current = newItemCursor; lastCursorRef.current = newItemCursor;
hookLogger.debug("Received item; cursor advanced", { console.log(
subscriptionKey, `[${subscriptionKey}] New message received with cursor:`,
cursor: lastCursorRef.current, lastCursorRef.current,
}); );
} }
const existing = itemMap.get(item[uniqKey]); const existing = itemMap.get(item[uniqKey]);
@ -323,27 +289,30 @@ export default function useStreamQueryWithSubscription(
}, },
}); });
// Save unsubscribe for cleanup on reruns/unmount // Cleanup on unmount or re-run
unsubscribeRef.current = unsubscribe; return () => {
console.log(`[${subscriptionKey}] Cleaning up subscription`);
// Note: cleanup is handled by the effect cleanup below. if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
unsubscribe();
};
} catch (error) { } catch (error) {
// Handle setup errors (like malformed queries) // Handle setup errors (like malformed queries)
hookLogger.error("Error setting up subscription", { console.error(
subscriptionKey, `[${subscriptionKey}] Error setting up subscription:`,
error, error,
}); );
subscriptionErrorRef.current = error; subscriptionErrorRef.current = error;
// Increment retry counter but don't exceed maxRetries // Increment retry counter but don't exceed maxRetries
retryCountRef.current = Math.min(retryCountRef.current + 1, maxRetries); retryCountRef.current = Math.min(retryCountRef.current + 1, maxRetries);
hookLogger.warn("Subscription setup error", { console.error(
subscriptionKey, `[${subscriptionKey}] Subscription setup error (attempt ${retryCountRef.current}/${maxRetries}):`,
attempt: retryCountRef.current,
maxRetries,
error, error,
}); );
// If we haven't reached max retries, trigger a retry // If we haven't reached max retries, trigger a retry
if (retryCountRef.current < maxRetries) { if (retryCountRef.current < maxRetries) {
@ -365,14 +334,16 @@ export default function useStreamQueryWithSubscription(
}, },
}); });
} catch (sentryError) { } catch (sentryError) {
hookLogger.error("Failed to report setup error to Sentry", { console.error("Failed to report to Sentry:", sentryError);
subscriptionKey,
error: sentryError,
});
} }
} }
// Cleanup is handled by the effect cleanup below. return () => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
};
} }
}, backoffDelay); }, backoffDelay);
@ -382,16 +353,6 @@ export default function useStreamQueryWithSubscription(
clearTimeout(timeoutIdRef.current); clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null; timeoutIdRef.current = null;
} }
if (unsubscribeRef.current) {
try {
hookLogger.debug("Cleaning up subscription", { subscriptionKey });
unsubscribeRef.current();
} catch (_error) {
// ignore
}
unsubscribeRef.current = null;
}
}; };
}, [ }, [
skip, skip,