feat: Implement push notification functionality in AddToCartButton, including subscription management and UI updates, and enhance translations for related error messages across multiple locales
This commit is contained in:
@@ -11,7 +11,20 @@ import RemoveIcon from "@mui/icons-material/Remove";
|
||||
import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import NotificationsIcon from "@mui/icons-material/Notifications";
|
||||
import NotificationsActiveIcon from "@mui/icons-material/NotificationsActive";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { withI18n } from "../i18n/withTranslation.js";
|
||||
import {
|
||||
isPushApiSupported,
|
||||
fetchPushConfiguration,
|
||||
registerPushServiceWorker,
|
||||
ensurePushSubscription,
|
||||
articlePushStatus,
|
||||
articlePushSubscribe,
|
||||
articlePushUnsubscribe,
|
||||
parseSubscribedStatus,
|
||||
parseSuccess,
|
||||
} from "../utils/articlePush.js";
|
||||
|
||||
if (!Array.isArray(window.cart)) window.cart = [];
|
||||
|
||||
@@ -25,9 +38,134 @@ class AddToCartButton extends Component {
|
||||
: 0,
|
||||
isEditing: false,
|
||||
editValue: "",
|
||||
pushInteractive: false,
|
||||
pushSubscribed: false,
|
||||
pushBusy: false,
|
||||
pushError: null,
|
||||
};
|
||||
}
|
||||
|
||||
kArtikelNumber = () => {
|
||||
const { id } = this.props;
|
||||
const n = typeof id === "number" ? id : parseInt(id, 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
};
|
||||
|
||||
refreshIncomingPushStatus = async () => {
|
||||
const { available, incoming } = this.props;
|
||||
if (available || !incoming) {
|
||||
this.setState({
|
||||
pushInteractive: false,
|
||||
pushSubscribed: false,
|
||||
pushError: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const kArtikel = this.kArtikelNumber();
|
||||
if (!kArtikel || !isPushApiSupported()) {
|
||||
this.setState({ pushInteractive: false });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const cfg = await fetchPushConfiguration();
|
||||
if (!cfg.configured || !cfg.publicKey) {
|
||||
this.setState({ pushInteractive: false });
|
||||
return;
|
||||
}
|
||||
await registerPushServiceWorker();
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
if (!subscription) {
|
||||
this.setState({
|
||||
pushInteractive: true,
|
||||
pushSubscribed: false,
|
||||
pushError: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const statusData = await articlePushStatus(kArtikel, subscription.endpoint);
|
||||
this.setState({
|
||||
pushInteractive: true,
|
||||
pushSubscribed: parseSubscribedStatus(statusData),
|
||||
pushError: null,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("AddToCartButton: incoming push init failed", e);
|
||||
this.setState({ pushInteractive: false });
|
||||
}
|
||||
};
|
||||
|
||||
handleIncomingPushClick = async () => {
|
||||
if (!this.state.pushInteractive || this.state.pushBusy) return;
|
||||
const kArtikel = this.kArtikelNumber();
|
||||
if (!kArtikel) return;
|
||||
const t = this.props.t;
|
||||
this.setState({ pushBusy: true, pushError: null });
|
||||
try {
|
||||
if (this.state.pushSubscribed) {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
if (!subscription) {
|
||||
this.setState({ pushSubscribed: false, pushBusy: false });
|
||||
return;
|
||||
}
|
||||
const res = await articlePushUnsubscribe(subscription.endpoint);
|
||||
if (parseSuccess(res)) {
|
||||
this.setState({ pushSubscribed: false });
|
||||
} else {
|
||||
this.setState({
|
||||
pushError:
|
||||
res?.message ||
|
||||
res?.error ||
|
||||
(t ? t("productDialogs.pushNotifyError") : ""),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const perm = await Notification.requestPermission();
|
||||
if (perm !== "granted") {
|
||||
this.setState({
|
||||
pushError: t
|
||||
? t("productDialogs.pushNotifyPermissionDenied")
|
||||
: "",
|
||||
pushBusy: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const cfg = await fetchPushConfiguration();
|
||||
if (!cfg.configured || !cfg.publicKey) {
|
||||
this.setState({
|
||||
pushError: t
|
||||
? t("productDialogs.pushNotifyServerDisabled")
|
||||
: "",
|
||||
pushBusy: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await registerPushServiceWorker();
|
||||
const subscription = await ensurePushSubscription(cfg.publicKey);
|
||||
const res = await articlePushSubscribe(kArtikel, subscription);
|
||||
if (parseSuccess(res)) {
|
||||
this.setState({ pushSubscribed: true });
|
||||
} else {
|
||||
this.setState({
|
||||
pushError:
|
||||
res?.message ||
|
||||
res?.error ||
|
||||
(t ? t("productDialogs.pushNotifyError") : ""),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("AddToCartButton: incoming push", e);
|
||||
this.setState({
|
||||
pushError:
|
||||
e.message || (t ? t("productDialogs.pushNotifyError") : ""),
|
||||
});
|
||||
} finally {
|
||||
this.setState({ pushBusy: false });
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.cart = () => {
|
||||
if (!Array.isArray(window.cart)) window.cart = [];
|
||||
@@ -37,6 +175,17 @@ class AddToCartButton extends Component {
|
||||
this.setState({ quantity: newQuantity });
|
||||
};
|
||||
window.addEventListener("cart", this.cart);
|
||||
this.refreshIncomingPushStatus();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.available !== this.props.available ||
|
||||
prevProps.incoming !== this.props.incoming ||
|
||||
prevProps.id !== this.props.id
|
||||
) {
|
||||
this.refreshIncomingPushStatus();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -134,46 +283,77 @@ class AddToCartButton extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { quantity, isEditing, editValue } = this.state;
|
||||
const {
|
||||
quantity,
|
||||
isEditing,
|
||||
editValue,
|
||||
pushInteractive,
|
||||
pushSubscribed,
|
||||
pushBusy,
|
||||
pushError,
|
||||
} = this.state;
|
||||
const { available, size, incoming, availableSupplier } = this.props;
|
||||
|
||||
// Button is disabled if product is not available
|
||||
if (!available) {
|
||||
if (incoming) {
|
||||
const dateStr = new Date(incoming).toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
const dateLabel = this.props.t
|
||||
? this.props.t("cart.availableFrom", { date: dateStr })
|
||||
: `Ab ${dateStr}`;
|
||||
|
||||
return (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size={size || "medium"}
|
||||
startIcon={<NotificationsIcon />}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
fontWeight: "bold",
|
||||
backgroundColor: "#ffeb3b",
|
||||
color: "#000000",
|
||||
whiteSpace: "nowrap",
|
||||
flexWrap: "nowrap",
|
||||
"& .MuiButton-label": {
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size={size || "medium"}
|
||||
onClick={pushInteractive ? this.handleIncomingPushClick : undefined}
|
||||
disabled={pushInteractive && pushBusy}
|
||||
startIcon={
|
||||
pushBusy ? (
|
||||
<CircularProgress size={18} sx={{ color: "inherit" }} />
|
||||
) : pushSubscribed ? (
|
||||
<NotificationsActiveIcon sx={{ color: "#2e7d32" }} />
|
||||
) : (
|
||||
<NotificationsIcon sx={{ color: "rgba(0,0,0,0.75)" }} />
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
fontWeight: "bold",
|
||||
backgroundColor: "#ffeb3b",
|
||||
color: "#000000",
|
||||
whiteSpace: "nowrap",
|
||||
flexWrap: "nowrap",
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: "#fdd835",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{this.props.t ? this.props.t('cart.availableFrom', {
|
||||
date: new Date(incoming).toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
}) : `Ab ${new Date(incoming).toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}`}
|
||||
</Button>
|
||||
"& .MuiButton-label": {
|
||||
whiteSpace: "nowrap",
|
||||
flexWrap: "nowrap",
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: "#fdd835",
|
||||
},
|
||||
...(pushInteractive && {
|
||||
cursor: "pointer",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{dateLabel}
|
||||
</Button>
|
||||
{pushError && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="error"
|
||||
sx={{ display: "block", mt: 0.5, textAlign: "center" }}
|
||||
>
|
||||
{pushError}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user