Created
August 8, 2023 11:05
-
-
Save MarceloPrado/eb3cc590b57f855ae0f084d656da0fea to your computer and use it in GitHub Desktop.
This gist describes how to build a global alert system that follows a similar API as RN's Alert.alert(). It can be cleaned up and improved, especially in the GlobalAlertManager side, but it's a good starting point.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; | |
/** | |
* This gist describes how to build a global alert system that follows a similar | |
* API as RN's Alert.alert(). | |
* | |
* This can be cleaned up and improved, especially in the GlobalAlertManager | |
* side, but it's a good starting point. | |
*/ | |
// Your entry point. | |
const AppWithProviders = () => ( | |
<BottomSheetModalProvider> | |
<App /> | |
<GlobalAlert /> | |
</BottomSheetModalProvider> | |
); | |
/** | |
* Path: src/components/GlobalAlert | |
*/ | |
import { globalAlertManager } from "./GlobalAlertManager"; | |
import { memo, useCallback, useEffect, useRef, useState } from "react"; | |
export interface GlobalAlertProps {} | |
export type GlobalAlertRef = { | |
present: (AlertProps: AlertProps) => void; | |
open: () => void; | |
dismiss: () => void; | |
}; | |
export const GlobalAlert = memo(({}: GlobalAlertProps) => { | |
const [alertProps, setAlertProps] = useState<AlertProps | undefined>( | |
undefined | |
); | |
const handlePresent = useCallback((newProps: AlertProps) => { | |
setAlertProps(newProps); | |
}, []); | |
const handleOpen = useCallback(() => { | |
setAlertProps((prev) => (prev ? { ...prev, isOpened: true } : undefined)); | |
}, []); | |
const handleDismiss = useCallback(() => { | |
setAlertProps((prev) => (prev ? { ...prev, isOpened: false } : undefined)); | |
}, []); | |
const alertRef = useRef<GlobalAlertRef>({ | |
open: handleOpen, | |
present: handlePresent, | |
dismiss: handleDismiss, | |
}); | |
useEffect(() => { | |
globalAlertManager.register(alertRef.current); | |
}, []); | |
// <Alert /> is your custom RN alert, e.g. a bottom-sheet modal. | |
return alertProps ? <Alert {...alertProps} /> : null; | |
}); | |
/** | |
* Path: src/components/GlobalAlertManager.ts | |
*/ | |
type AlertButton = { | |
title: string; | |
onPress: () => void; | |
variant?: ButtonProps["variant"]; | |
}; | |
type AlertOptions = { | |
title: string; | |
description?: string; | |
} & ( | |
| { | |
dismissTitle?: string; | |
onPressDismiss?: () => void; | |
actions?: never; | |
actionsDirection?: never; | |
} | |
| { | |
dismissTitle?: never; | |
onPressDismiss?: never; | |
actions: AlertButton[]; | |
actionsDirection?: AlertProps["actionsDirection"]; | |
} | |
); | |
class GlobalAlertManager { | |
private alertRef: GlobalAlertRef | undefined; | |
private retryCount = 0; | |
private readonly maxRetries = 2; | |
private dismissReason: "retryButton" | "unknown" | "dismissButton" = | |
"unknown"; | |
constructor(private readonly logger = createLogger("AlertManager")) {} | |
/** | |
* Register the alert globally | |
*/ | |
public register(ref: GlobalAlertRef) { | |
this.logger.info("Registering a global alert"); | |
this.alertRef = ref; | |
} | |
private setIsOpen(isOpen: boolean) { | |
if (!this.alertRef) { | |
throw new Error("You must first register your alert in the AlertManager"); | |
} | |
if (isOpen) { | |
this.alertRef.open(); | |
} else { | |
this.onPressDismiss(); | |
} | |
} | |
private onPressDismiss() { | |
if (!this.alertRef) { | |
throw new Error("You must first register your alert in the AlertManager"); | |
} | |
if (this.dismissReason === "dismissButton") { | |
this.logger.info("Dismissing retry alert (reason: dismiss button)"); | |
} else if (this.dismissReason === "unknown") { | |
this.logger.info("Dismissing retry alert (reason: bottom sheet cb)"); | |
} | |
if (this.dismissReason !== "retryButton") { | |
this.logger.info("Resetting retry count"); | |
this.retryCount = 0; | |
} | |
this.alertRef.dismiss(); | |
} | |
private onPressRetry(onRetry: () => void) { | |
if (!this.alertRef) { | |
throw new Error("You must first register your alert in the AlertManager"); | |
} | |
this.retryCount += 1; | |
this.logger.info(`Retrying, attempt ${this.retryCount}/${this.maxRetries}`); | |
this.alertRef.dismiss(); | |
onRetry(); | |
} | |
/** | |
* Displays a retryable alert. | |
*/ | |
public alertWithRetry({ | |
title, | |
description = "Sugerimos tentar mais uma vez", | |
onRetry, | |
}: { | |
title: string; | |
description?: string; | |
onRetry: () => void; | |
}) { | |
if (!this.alertRef) { | |
throw new Error("You must first register your alert in the AlertManager"); | |
} | |
const canRetry = this.retryCount < this.maxRetries; | |
if (!canRetry) { | |
this.logger.info("Retries exhausted"); | |
} | |
this.dismissReason = "unknown"; | |
this.alertRef.present({ | |
title, | |
description, | |
isOpened: true, | |
setIsOpened: (isOpen: boolean) => { | |
this.setIsOpen(isOpen); | |
}, | |
actions: [ | |
<Button | |
key="dismiss" | |
onPress={() => { | |
this.dismissReason = "dismissButton"; | |
this.onPressDismiss(); | |
}} | |
variant="secondary" | |
> | |
Fechar | |
</Button>, | |
canRetry ? ( | |
<Button | |
key="retry" | |
onPress={() => { | |
this.dismissReason = "retryButton"; | |
this.onPressRetry(onRetry); | |
}} | |
variant="primary" | |
> | |
Tentar de novo | |
</Button> | |
) : null, | |
], | |
}); | |
} | |
/** | |
* Displays a simple alert with a dismiss button. | |
*/ | |
public alert({ | |
title, | |
description = "Sugerimos tentar mais uma vez", | |
dismissTitle = "Fechar", | |
onPressDismiss, | |
actions: options, | |
actionsDirection = "horizontal", | |
}: AlertOptions) { | |
if (!this.alertRef) { | |
throw new Error("You must first register your alert in the AlertManager"); | |
} | |
const actions = Array.isArray(options) | |
? options.map((option) => ( | |
<Button | |
grow | |
key={option.title} | |
onPress={() => { | |
this.alertRef?.dismiss(); | |
option.onPress(); | |
}} | |
variant={option.variant ?? "secondary"} | |
> | |
{option.title} | |
</Button> | |
)) | |
: [ | |
<Button | |
grow | |
key="dismiss" | |
onPress={() => { | |
this.alertRef?.dismiss(); | |
onPressDismiss?.(); | |
}} | |
variant="primary" | |
> | |
{dismissTitle} | |
</Button>, | |
]; | |
this.alertRef.present({ | |
title, | |
description, | |
isOpened: true, | |
setIsOpened: (isOpen: boolean) => { | |
if (isOpen) { | |
this.alertRef?.open(); | |
} else { | |
this.alertRef?.dismiss(); | |
} | |
}, | |
actions, | |
actionsDirection, | |
}); | |
} | |
} | |
export const globalAlertManager = new GlobalAlertManager(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment