Skip to content

Instantly share code, notes, and snippets.

@MarceloPrado
Created August 8, 2023 11:05
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MarceloPrado/eb3cc590b57f855ae0f084d656da0fea to your computer and use it in GitHub Desktop.
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.
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