stripe embedded and other fixes

This commit is contained in:
Thomas Demirdjian
2025-11-18 11:40:39 +01:00
parent 016f7c27ac
commit 7ab8685180
17 changed files with 2346 additions and 2504 deletions
+25 -11
View File
@@ -16,6 +16,7 @@ import {
StyleSheet,
Text,
View,
useWindowDimensions,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import {
@@ -74,6 +75,8 @@ const MusicDetails = ({ route }) => {
const hasAutoPlayedRef = React.useRef(false);
const { isLooping, setLooping } = usePlayer() || {};
const isWeb = Platform.OS === "web";
const { width: windowWidth = 0 } = useWindowDimensions();
const isCompactLayout = (windowWidth || 0) < 1200;
const handleBackPress = useCallback(() => {
goBack();
}, []);
@@ -964,6 +967,7 @@ const MusicDetails = ({ route }) => {
headerType="NAVIGATION"
hideBackButton={isWeb}
topStickyContent={renderWebBackButton}
width={isCompactLayout ? "100%" : undefined}
title={action === "userProfile" ? "Mon profil" : "Détail musique"}
backgroundImg={
action === "userProfile"
@@ -993,11 +997,12 @@ const MusicDetails = ({ route }) => {
style={{
flex: 1,
paddingBottom: gutters * 2,
flexDirection: "row",
flexDirection: isCompactLayout ? "column" : "row",
justifyContent: "center",
alignItems: "center",
alignItems: isCompactLayout ? "stretch" : "center",
height: "auto",
gap: 24,
gap: isCompactLayout ? 16 : 24,
width: "100%",
}}
>
{/* Left column: player */}
@@ -1005,13 +1010,14 @@ const MusicDetails = ({ route }) => {
intensity={40}
style={{
flex: 1,
paddingRight: 8,
height: 400,
paddingRight: isCompactLayout ? 0 : 8,
height: isCompactLayout ? undefined : 400,
borderRadius: 12,
padding: 10,
borderWidth: 1,
borderColor: Palette.transparentWhite,
maxWidth: "50%",
maxWidth: isCompactLayout ? "100%" : "50%",
width: "100%",
}}
>
<View>
@@ -1159,20 +1165,25 @@ const MusicDetails = ({ route }) => {
intensity={40}
style={{
flex: 1,
paddingLeft: 8,
paddingLeft: isCompactLayout ? 0 : 8,
padding: 10,
borderWidth: 1,
borderColor: Palette.transparentWhite,
borderRadius: 12,
height: 400,
maxWidth: "50%",
height: isCompactLayout ? undefined : 400,
maxWidth: isCompactLayout ? "100%" : "50%",
width: "100%",
marginTop: isCompactLayout ? 12 : 0,
}}
>
{sections.length ? (
<ScrollView
ref={lyricsRef}
style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 40, height: 400 }}
contentContainerStyle={{
paddingBottom: 40,
...(isCompactLayout ? {} : { height: 400 }),
}}
>
{sections.map((section, sectionIdx) => (
<View
@@ -1231,7 +1242,10 @@ const MusicDetails = ({ route }) => {
) : description?.length > 0 ? (
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 40 }}
contentContainerStyle={{
paddingBottom: 40,
...(isCompactLayout ? {} : { height: 400 }),
}}
>
<Text
style={{
+233 -72
View File
@@ -8,7 +8,7 @@ import React, {
useRef,
useState,
} from "react";
import { Text, TouchableOpacity, View } from "react-native";
import { BackHandler, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Svg, { Circle } from "react-native-svg";
import alert from "../../components/Alert";
@@ -54,6 +54,10 @@ const RecordPlayback = ({ route }) => {
const checkSongEndRef = useRef(null);
const stopRequestedRef = useRef(false);
const restartRequestedRef = useRef(false);
const manualRestartInFlightRef = useRef(false);
const stopRequestedAtRef = useRef(0);
const activeRecordingPromiseRef = useRef(null);
const exitRequestedRef = useRef(false);
const startedRef = useRef(false); // empêche les doubles démarrages
const countdownActiveRef = useRef(false); // évite le déclenchement avant 1er tick
const playbackStartedRef = useRef(false); // devient vrai lorsque l'audio progresse réellement
@@ -259,45 +263,58 @@ const RecordPlayback = ({ route }) => {
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Reset complet
const resetSession = useCallback(async () => {
try {
if (countdownTimerRef.current) {
clearInterval(countdownTimerRef.current);
countdownTimerRef.current = null;
}
if (listenTimerRef.current) {
clearInterval(listenTimerRef.current);
listenTimerRef.current = null;
log("listenTimerRef cleared");
}
if (checkSongEndRef.current) {
clearInterval(checkSongEndRef.current);
checkSongEndRef.current = null;
log("checkSongEndRef cleared");
}
const resetSession = useCallback(
async ({
preserveSongEndWatcher = false,
preserveStopRequest = false,
} = {}) => {
try {
if (countdownTimerRef.current) {
clearInterval(countdownTimerRef.current);
countdownTimerRef.current = null;
}
if (listenTimerRef.current) {
clearInterval(listenTimerRef.current);
listenTimerRef.current = null;
log("listenTimerRef cleared");
}
if (checkSongEndRef.current) {
if (preserveSongEndWatcher) {
log("checkSongEndRef preserved");
} else {
clearInterval(checkSongEndRef.current);
checkSongEndRef.current = null;
log("checkSongEndRef cleared");
}
}
startedRef.current = false;
stopRequestedRef.current = false;
countdownActiveRef.current = false;
listenedMsRef.current = 0;
incrementDoneRef.current = false;
playbackStartedRef.current = false;
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 };
log("Session reset");
startedRef.current = false;
if (!preserveStopRequest) {
stopRequestedRef.current = false;
stopRequestedAtRef.current = 0;
}
countdownActiveRef.current = false;
listenedMsRef.current = 0;
incrementDoneRef.current = false;
playbackStartedRef.current = false;
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 };
log("Session reset");
setIsPreparing(false);
setIsRecording(false);
setShowProgress(false);
setCountdown(0);
setIsPreparing(false);
setIsRecording(false);
setShowProgress(false);
setCountdown(0);
if (player) {
try {
if (player.playing) await player.pause?.();
await player.seekTo?.(0);
} catch (_) {}
}
} catch (_) {}
}, [player]);
if (player) {
try {
if (player.playing) await player.pause?.();
await player.seekTo?.(0);
} catch (_) {}
}
} catch (_) {}
},
[player]
);
const resetSessionRef = useRef(resetSession);
useEffect(() => {
@@ -307,6 +324,7 @@ const RecordPlayback = ({ route }) => {
useFocusEffect(
useCallback(() => {
log("Screen focused, resetting session");
exitRequestedRef.current = false;
void resetSessionRef.current?.();
return () => {
log("Screen blurred, stopping playback");
@@ -337,15 +355,47 @@ const RecordPlayback = ({ route }) => {
}, [setLooping])
);
const discardRecordingFile = useCallback(async (uri, reason = "") => {
if (!uri) return;
try {
const info = await FileSystem.getInfoAsync(uri);
if (info?.exists) {
await FileSystem.deleteAsync(uri, { idempotent: true });
log("Discarded recording file", { reason: reason || "cleanup" });
}
} catch (error) {
log("Failed to discard recording", {
reason: reason || "cleanup",
message: error?.message || String(error || ""),
});
}
}, []);
// Lancer le compte à rebours (le tick décrémente uniquement)
const startCountdownThenRecord = async () => {
const startCountdownThenRecord = async ({
preserveRestartFlag = false,
preserveSongEndWatcher = false,
preserveStopRequest = false,
} = {}) => {
if (!songUrl) {
log("startCountdownThenRecord aborted: missing song URL");
return;
}
log("startCountdownThenRecord invoked", { projectId, songUrl });
await resetSession();
restartRequestedRef.current = false;
log("startCountdownThenRecord invoked", {
projectId,
songUrl,
preserveRestartFlag,
preserveSongEndWatcher,
preserveStopRequest,
});
await resetSession({
preserveSongEndWatcher,
preserveStopRequest,
});
if (!preserveRestartFlag) {
restartRequestedRef.current = false;
manualRestartInFlightRef.current = false;
}
setIsPreparing(true);
setShowProgress(false);
setCountdown(5);
@@ -387,6 +437,7 @@ const RecordPlayback = ({ route }) => {
const startRecordingWithMusic = async () => {
try {
stopRequestedRef.current = false;
stopRequestedAtRef.current = 0;
listenedMsRef.current = 0;
incrementDoneRef.current = false;
@@ -399,6 +450,18 @@ const RecordPlayback = ({ route }) => {
songUrl,
hasCamera: !!cameraRef.current,
});
if (activeRecordingPromiseRef.current) {
log("Waiting for previous recording to finish before starting a new one");
try {
await activeRecordingPromiseRef.current;
} catch (error) {
log("Previous recording promise rejected", {
message: error?.message || String(error || ""),
});
}
}
const recordPromise = (() => {
const camera = cameraRef.current;
if (!camera) throw new Error("Caméra indisponible");
@@ -451,6 +514,7 @@ const RecordPlayback = ({ route }) => {
"L'enregistrement vidéo n'est pas supporté sur cet appareil."
);
})();
activeRecordingPromiseRef.current = recordPromise;
if (player && songUrl) {
try {
@@ -501,7 +565,18 @@ const RecordPlayback = ({ route }) => {
log("Song end watcher armed");
checkSongEndRef.current = setInterval(() => {
try {
if (!player || stopRequestedRef.current) return;
if (!player) return;
if (stopRequestedRef.current) {
const sinceLastRequest = Date.now() - (stopRequestedAtRef.current || 0);
if (sinceLastRequest >= 1200) {
stopRequestedAtRef.current = Date.now();
try {
cameraRef.current?.stopRecording?.();
log("stopRecording retried while awaiting stop");
} catch (_) {}
}
return;
}
const duration = (player?.duration || 0) * 1000;
const currentTime = (player?.currentTime || 0) * 1000;
if (
@@ -524,10 +599,7 @@ const RecordPlayback = ({ route }) => {
playing: player?.playing,
});
stopRequestedRef.current = true;
if (checkSongEndRef.current) {
clearInterval(checkSongEndRef.current);
checkSongEndRef.current = null;
}
stopRequestedAtRef.current = Date.now();
try {
cameraRef.current?.stopRecording?.();
log("stopRecording triggered");
@@ -539,6 +611,9 @@ const RecordPlayback = ({ route }) => {
const video = await recordPromise;
log("Recording promise resolved", { hasVideo: !!video?.uri });
activeRecordingPromiseRef.current = null;
stopRequestedRef.current = false;
stopRequestedAtRef.current = 0;
if (checkSongEndRef.current) {
clearInterval(checkSongEndRef.current);
@@ -559,30 +634,31 @@ const RecordPlayback = ({ route }) => {
log("Recording flow completed", { hasVideo: !!video?.uri });
playbackStartedRef.current = false;
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 };
const exitRequested = exitRequestedRef.current;
const shouldRestart = restartRequestedRef.current;
if (exitRequested) {
exitRequestedRef.current = false;
restartRequestedRef.current = false;
manualRestartInFlightRef.current = false;
await discardRecordingFile(video?.uri, "exit");
log("Recording aborted before completion, skipping navigation");
return;
}
if (shouldRestart) {
restartRequestedRef.current = false;
}
if (video?.uri && shouldRestart) {
try {
const info = await FileSystem.getInfoAsync(video.uri);
if (info?.exists) {
await FileSystem.deleteAsync(video.uri, { idempotent: true });
log("Discarded interim recording file");
}
} catch (error) {
log("Failed to discard interim recording", {
message: error?.message || String(error || ""),
await discardRecordingFile(video?.uri, "restart");
const manualRestartPending = manualRestartInFlightRef.current;
manualRestartInFlightRef.current = false;
if (manualRestartPending) {
log("Manual restart already scheduled, waiting for countdown");
} else {
log("Restart requested, relaunching countdown");
requestAnimationFrame(() => {
void startCountdownThenRecord();
});
}
}
if (shouldRestart) {
log("Restart requested, relaunching countdown");
requestAnimationFrame(() => {
void startCountdownThenRecord();
});
return;
}
@@ -602,6 +678,9 @@ const RecordPlayback = ({ route }) => {
message: e?.message || String(e || ""),
});
console.log("RecordPlayback error:", e);
activeRecordingPromiseRef.current = null;
stopRequestedRef.current = false;
stopRequestedAtRef.current = 0;
setIsRecording(false);
setIsPreparing(false);
setShowProgress(false);
@@ -617,13 +696,27 @@ const RecordPlayback = ({ route }) => {
}
playbackStartedRef.current = false;
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 };
const exitRequested = exitRequestedRef.current;
const shouldRestart = restartRequestedRef.current;
if (exitRequested) {
exitRequestedRef.current = false;
restartRequestedRef.current = false;
manualRestartInFlightRef.current = false;
log("Recording aborted, skipping error handling");
return;
}
if (shouldRestart) {
restartRequestedRef.current = false;
log("Restart requested despite error, restarting flow");
requestAnimationFrame(() => {
void startCountdownThenRecord();
});
const manualRestartPending = manualRestartInFlightRef.current;
manualRestartInFlightRef.current = false;
if (manualRestartPending) {
log("Manual restart already scheduled after error");
} else {
log("Restart requested despite error, restarting flow");
requestAnimationFrame(() => {
void startCountdownThenRecord();
});
}
return;
}
// Inform the user when using a simulator where recording isn't supported
@@ -648,25 +741,94 @@ const RecordPlayback = ({ route }) => {
}
};
const handleBackPress = useCallback(() => {
const busy = isPreparing || isRecording;
log("Back button pressed", { isPreparing, isRecording, busy });
if (busy) {
exitRequestedRef.current = true;
restartRequestedRef.current = false;
manualRestartInFlightRef.current = false;
stopRequestedRef.current = true;
stopRequestedAtRef.current = Date.now();
if (isPreparing && countdownTimerRef.current) {
clearInterval(countdownTimerRef.current);
countdownTimerRef.current = null;
}
try {
if (player?.playing) {
const maybePromise = player.pause?.();
if (maybePromise && typeof maybePromise.catch === "function") {
maybePromise.catch(() => {});
}
}
} catch (_) {}
try {
if (isRecording) {
cameraRef.current?.stopRecording?.();
}
} catch (_) {}
} else {
exitRequestedRef.current = false;
}
try {
goBack();
} catch (_) {}
return true;
}, [goBack, isPreparing, isRecording, player]);
useFocusEffect(
useCallback(() => {
const subscription = BackHandler.addEventListener(
"hardwareBackPress",
() => handleBackPress()
);
return () => subscription.remove();
}, [handleBackPress])
);
const handleRestartRecording = async () => {
try {
const hasRecordingPending = !!activeRecordingPromiseRef.current;
log("Restart button pressed", {
isPreparing,
isRecording,
hasRecordingPending,
});
if (isPreparing || !isRecording) {
if (isPreparing) {
if (hasRecordingPending) {
await startCountdownThenRecord({
preserveRestartFlag: true,
preserveSongEndWatcher: true,
preserveStopRequest: true,
});
return;
}
restartRequestedRef.current = false;
manualRestartInFlightRef.current = false;
await startCountdownThenRecord();
return;
}
if (!isRecording) {
restartRequestedRef.current = false;
manualRestartInFlightRef.current = false;
await startCountdownThenRecord();
return;
}
restartRequestedRef.current = true;
manualRestartInFlightRef.current = true;
stopRequestedRef.current = true;
stopRequestedAtRef.current = Date.now();
try {
if (player?.playing) await player.pause?.();
} catch (_) {}
try {
cameraRef.current?.stopRecording?.();
} catch (_) {}
await startCountdownThenRecord({
preserveRestartFlag: true,
preserveSongEndWatcher: true,
preserveStopRequest: true,
});
} catch (_) {}
};
@@ -693,7 +855,7 @@ const RecordPlayback = ({ route }) => {
paddingBottom: gutters * 2,
}}
>
<MusicLandHeader progress={9} onPressBack={goBack} />
<MusicLandHeader progress={9} onPressBack={handleBackPress} />
<View style={{ marginTop: 12, alignItems: "flex-end" }}>
<CameraFacingSelector
@@ -869,7 +1031,6 @@ const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => {
height={size}
style={{
transform: [{ rotate: "-90deg" }],
backgroundColor: "#ffffff3d",
borderRadius: size / 2,
}}
>
+43 -1
View File
@@ -202,9 +202,51 @@ const SongReady = () => {
const handleRegeneratePress = () => {
if (isWeb) {
const descriptionTextStyle = {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
};
const amountTextStyle = {
...descriptionTextStyle,
fontFamily: FONT_FAMILY.InterSemiBold,
};
alert(
"Re-générer le morceau",
`Tu vas pouvoir modifier tes choix pour re-générer ${SONG_OPTIONS_PER_GENERATION} nouveaux morceaux pour ${MUSIC_GENERATION_COIN_COST} crédits.\n\nLes crédits seront utilisés lors de l'étape de génération.`,
(
<View
style={{
width: "100%",
alignItems: "center",
gap: 12,
}}
>
<Text style={descriptionTextStyle}>
{`Tu vas pouvoir modifier tes choix pour re-générer ${SONG_OPTIONS_PER_GENERATION} nouveaux morceaux.`}
</Text>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexWrap: "wrap",
gap: 6,
}}
>
<Text style={descriptionTextStyle}>Cette action coûte</Text>
<CreditAmount
value={MUSIC_GENERATION_COIN_COST}
iconSize={18}
textStyle={amountTextStyle}
/>
</View>
<Text style={descriptionTextStyle}>
Les crédits seront utilisés lors de l'étape de génération.
</Text>
</View>
),
[
{
text: "Annuler",