From 387de3b99cbc1c8caa2a21e59a7b537c81288e8f Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 15:36:45 +0530 Subject: [PATCH 01/17] add IOS push screen --- src/dashboard/Dashboard.js | 3 + src/dashboard/DashboardView.react.js | 1 + .../Push/P12CertificateModal.react.js | 134 ++++++ src/dashboard/Push/P8AuthKeyModal.react.js | 190 ++++++++ src/dashboard/Push/PushiOSSettings.react.js | 450 ++++++++++++++++++ src/dashboard/Push/PushiOSSettings.scss | 260 ++++++++++ src/lib/ParseApp.js | 83 ++++ 7 files changed, 1121 insertions(+) create mode 100644 src/dashboard/Push/P12CertificateModal.react.js create mode 100644 src/dashboard/Push/P8AuthKeyModal.react.js create mode 100644 src/dashboard/Push/PushiOSSettings.react.js create mode 100644 src/dashboard/Push/PushiOSSettings.scss diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js index 6029cf16c..df422bc98 100644 --- a/src/dashboard/Dashboard.js +++ b/src/dashboard/Dashboard.js @@ -83,6 +83,7 @@ const LazyDatabaseProfile = lazy(() => import('./DatabaseProfiler/DatabaseProfil const LazyEmailVerification = lazy(() => import('./Notification/EmailVerification.react')); const LazyEmailPasswordReset = lazy(() => import('./Notification/EmailPasswordReset.react')); const LazyPushAndroidSettings = lazy(() => import('./Push/PushAndroidSettings.react')); +const LazyPushiOSSettings = lazy(() => import('./Push/PushiOSSettings.react')); async function fetchHubUser() { @@ -164,6 +165,7 @@ const preloadMap = { emailVerification: () => import('./Notification/EmailVerification.react'), emailPasswordReset: () => import('./Notification/EmailPasswordReset.react'), pushAndroidSettings: () => import('./Push/PushAndroidSettings.react'), + pushiOSSettings: () => import('./Push/PushiOSSettings.react'), }; // Preload all routes with proper error handling and logging @@ -507,6 +509,7 @@ class Dashboard extends React.Component { } /> } /> } /> + } /> } /> } /> diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js index f5b990a08..184063432 100644 --- a/src/dashboard/DashboardView.react.js +++ b/src/dashboard/DashboardView.react.js @@ -356,6 +356,7 @@ export default class DashboardView extends React.Component { { name: 'Past Pushes', link: '/push/activity' }, { name: 'Audiences', link: '/push/audiences' }, { name: 'Android', link: '/push/android-settings' }, + { name: 'iOS', link: '/push/ios-settings' }, ], }, ]; diff --git a/src/dashboard/Push/P12CertificateModal.react.js b/src/dashboard/Push/P12CertificateModal.react.js new file mode 100644 index 000000000..cec63c1c8 --- /dev/null +++ b/src/dashboard/Push/P12CertificateModal.react.js @@ -0,0 +1,134 @@ +import React, { useState, useCallback } from 'react'; +import B4aModal from 'components/B4aModal/B4aModal.react'; +import Field from 'components/Field/Field.react'; +import Label from 'components/Label/Label.react'; +import FileInput from 'components/FileInput/FileInput.react'; +import Dropdown from 'components/Dropdown/Dropdown.react'; +import Option from 'components/Dropdown/Option.react'; +import RadioButton from 'components/RadioButton/RadioButton.react'; +import FormNote from 'components/FormNote/FormNote.react'; +import styles from './PushiOSSettings.scss'; + +function validate(fields) { + const errors = {}; + + if (!fields.file) { + errors.file = 'APNs certificate file is required.'; + } else if (!fields.file.name.endsWith('.p12')) { + errors.file = 'File must have a .p12 extension.'; + } + + if (!fields.deviceType) { + errors.deviceType = 'Device type is required.'; + } + + return errors; +} + +const P12CertificateModal = ({ deviceTypes, onSave, onClose }) => { + const [file, setFile] = useState(null); + const [deviceType, setDeviceType] = useState('ios'); + const [production, setProduction] = useState(true); + const [errors, setErrors] = useState({}); + const [saving, setSaving] = useState(false); + const [serverError, setServerError] = useState(null); + + const handleSubmit = useCallback(async () => { + const fields = { file, deviceType }; + const validationErrors = validate(fields); + setErrors(validationErrors); + + if (Object.keys(validationErrors).length > 0) { + return; + } + + setSaving(true); + setServerError(null); + try { + await onSave(file, deviceType, production); + } catch (err) { + const msg = typeof err === 'string' ? err + : (err && err.error) || (err && err.message) || 'Failed to save certificate.'; + setServerError(msg); + setSaving(false); + } + }, [file, deviceType, production, onSave]); + + const firstError = Object.values(errors).find(Boolean); + + return ( + + } + input={ +
+ { setFile(f); setErrors(prev => ({ ...prev, file: undefined })); setServerError(null); }} + accept=".p12,application/x-pkcs12" + value={file ? { name: file.name } : undefined} + /> +
+ } + /> + } + input={ + { setDeviceType(value); setErrors(prev => ({ ...prev, deviceType: undefined })); }} + placeHolder="Select device type" + dark={false} + fixed={true} + > + {deviceTypes.map(type => ( + + ))} + + } + /> + } + input={ +
+ + +
+ } + /> + + {firstError} + + + {serverError} + +
+ ); +}; + +export default P12CertificateModal; diff --git a/src/dashboard/Push/P8AuthKeyModal.react.js b/src/dashboard/Push/P8AuthKeyModal.react.js new file mode 100644 index 000000000..7d9d7ef9d --- /dev/null +++ b/src/dashboard/Push/P8AuthKeyModal.react.js @@ -0,0 +1,190 @@ +import React, { useState, useCallback } from 'react'; +import B4aModal from 'components/B4aModal/B4aModal.react'; +import Field from 'components/Field/Field.react'; +import Label from 'components/Label/Label.react'; +import TextInput from 'components/TextInput/TextInput.react'; +import FileInput from 'components/FileInput/FileInput.react'; +import Dropdown from 'components/Dropdown/Dropdown.react'; +import Option from 'components/Dropdown/Option.react'; +import RadioButton from 'components/RadioButton/RadioButton.react'; +import FormNote from 'components/FormNote/FormNote.react'; +import styles from './PushiOSSettings.scss'; + +function validate(fields) { + const errors = {}; + + if (!fields.file) { + errors.file = 'APNs auth key file is required.'; + } else if (!fields.file.name.endsWith('.p8')) { + errors.file = 'File must have a .p8 extension.'; + } + + if (!fields.keyId || !fields.keyId.trim()) { + errors.keyId = 'Key ID is required.'; + } else if (fields.keyId.trim().length !== 10) { + errors.keyId = 'Key ID must be exactly 10 characters.'; + } + + if (!fields.teamId || !fields.teamId.trim()) { + errors.teamId = 'Team ID is required.'; + } else if (fields.teamId.trim().length !== 10) { + errors.teamId = 'Team ID must be exactly 10 characters.'; + } + + if (!fields.bundleId || !fields.bundleId.trim()) { + errors.bundleId = 'Bundle ID is required.'; + } + + if (!fields.deviceType) { + errors.deviceType = 'Device type is required.'; + } + + return errors; +} + +const P8AuthKeyModal = ({ deviceTypes, onSave, onClose }) => { + const [file, setFile] = useState(null); + const [keyId, setKeyId] = useState(''); + const [teamId, setTeamId] = useState(''); + const [bundleId, setBundleId] = useState(''); + const [deviceType, setDeviceType] = useState('ios'); + const [production, setProduction] = useState(true); + const [errors, setErrors] = useState({}); + const [saving, setSaving] = useState(false); + const [serverError, setServerError] = useState(null); + + const handleSubmit = useCallback(async () => { + const fields = { file, keyId, teamId, bundleId, deviceType }; + const validationErrors = validate(fields); + setErrors(validationErrors); + + if (Object.keys(validationErrors).length > 0) { + return; + } + + setSaving(true); + setServerError(null); + try { + await onSave(file, keyId.trim(), teamId.trim(), bundleId.trim(), deviceType, production); + } catch (err) { + const msg = typeof err === 'string' ? err + : (err && err.error) || (err && err.message) || 'Failed to save authentication key.'; + setServerError(msg); + setSaving(false); + } + }, [file, keyId, teamId, bundleId, deviceType, production, onSave]); + + const firstError = Object.values(errors).find(Boolean); + + return ( + + } + input={ +
+ { setFile(f); setErrors(prev => ({ ...prev, file: undefined })); setServerError(null); }} + accept=".p8" + value={file ? { name: file.name } : undefined} + /> +
+ } + /> + } + input={ + { setKeyId(value); setErrors(prev => ({ ...prev, keyId: undefined })); }} + /> + } + /> + } + input={ + { setTeamId(value); setErrors(prev => ({ ...prev, teamId: undefined })); }} + /> + } + /> + } + input={ + { setBundleId(value); setErrors(prev => ({ ...prev, bundleId: undefined })); }} + /> + } + /> + } + input={ + { setDeviceType(value); setErrors(prev => ({ ...prev, deviceType: undefined })); }} + placeHolder="Select device type" + dark={false} + fixed={true} + > + {deviceTypes.map(type => ( + + ))} + + } + /> + } + input={ +
+ + +
+ } + /> + + {firstError} + + + {serverError} + +
+ ); +}; + +export default P8AuthKeyModal; diff --git a/src/dashboard/Push/PushiOSSettings.react.js b/src/dashboard/Push/PushiOSSettings.react.js new file mode 100644 index 000000000..78788e9ae --- /dev/null +++ b/src/dashboard/Push/PushiOSSettings.react.js @@ -0,0 +1,450 @@ +import React from 'react'; +import { withRouter } from 'lib/withRouter'; +import Toolbar from 'components/Toolbar/Toolbar.react'; +import DashboardView from 'dashboard/DashboardView.react'; +import B4aLoaderContainer from 'components/B4aLoaderContainer/B4aLoaderContainer.react'; +import B4aModal from 'components/B4aModal/B4aModal.react'; +import Button from 'components/Button/Button.react'; +import Icon from 'components/Icon/Icon.react'; +import EmptyGhostState from 'components/EmptyGhostState/EmptyGhostState.react'; +import P8AuthKeyModal from './P8AuthKeyModal.react'; +import P12CertificateModal from './P12CertificateModal.react'; +import styles from './PushiOSSettings.scss'; + +const getErrorMessage = (err, fallback) => { + if (typeof err === 'string' && err.trim()) { + return err; + } + const responseData = err?.response?.data; + if (typeof responseData?.error === 'string' && responseData.error.trim()) { + return responseData.error; + } + if (typeof err?.message === 'string' && err.message.trim()) { + return err.message; + } + return fallback; +}; + +const formatDate = (isoString) => { + if (!isoString) { + return 'N/A'; + } + const d = new Date(isoString); + if (isNaN(d.getTime())) { + return 'N/A'; + } + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const month = months[d.getMonth()]; + const day = String(d.getDate()).padStart(2, '0'); + const year = d.getFullYear(); + return `${month}/${day}/${year}`; +}; + +@withRouter +class PushiOSSettings extends DashboardView { + constructor() { + super(); + this.section = 'Notification'; + this.subsection = 'iOS'; + this.state = { + isLoading: true, + loadingError: null, + p8Certificates: [], + p12Certificates: [], + deviceTypes: [], + disableP12: false, + incompatiblePS: false, + hasPermission: true, + showP8Modal: false, + showP12Modal: false, + showDeleteModal: false, + deleteTarget: null, + isDeleting: false, + deleteError: null, + }; + } + + componentDidMount() { + this._isMounted = true; + this.loadConfig(); + } + + componentWillUnmount() { + this._isMounted = false; + clearTimeout(this._deleteErrorTimer); + } + + async loadConfig() { + try { + const data = await this.context.getPushIOSConfig(); + if (!this._isMounted) { + return; + } + const hasPermission = !data.featuresPermission + || data.featuresPermission.pushIOSSettings === 'Write'; + + const p12Certificates = []; + if (data.cert && typeof data.cert === 'object') { + for (const [deviceType, certs] of Object.entries(data.cert)) { + if (Array.isArray(certs)) { + certs.forEach(c => p12Certificates.push({ ...c, deviceType })); + } + } + } + + const p8Certificates = []; + if (data.authKey && typeof data.authKey === 'object') { + for (const [deviceType, certs] of Object.entries(data.authKey)) { + if (Array.isArray(certs)) { + certs.forEach(c => p8Certificates.push({ + _id: c._id, + topic: c.topic, + keyId: c.token?.keyId, + teamId: c.token?.teamId, + production: c.production, + deviceType, + })); + } + } + } + + this.setState({ + isLoading: false, + loadingError: null, + p8Certificates, + p12Certificates, + deviceTypes: data.deviceTypes || [], + disableP12: !!data.disableP12, + incompatiblePS: !!data.isCompatiblePS, + hasPermission, + }); + } catch (err) { + if (this._isMounted) { + this.setState({ + isLoading: false, + loadingError: getErrorMessage(err, 'Failed to load iOS push settings.'), + }); + } + } + } + + handleSaveP8 = async (file, keyId, teamId, bundleId, deviceType, production) => { + await this.context.uploadP8AuthKey(file, keyId, teamId, bundleId, deviceType, production); + this.setState({ showP8Modal: false, isLoading: true }); + this.loadConfig(); + }; + + handleSaveP12 = async (file, deviceType, production) => { + await this.context.uploadP12Certificate(file, deviceType, production); + this.setState({ showP12Modal: false, isLoading: true }); + this.loadConfig(); + }; + + handleCloseP8Modal = () => { + this.setState({ showP8Modal: false }); + }; + + handleCloseP12Modal = () => { + this.setState({ showP12Modal: false }); + }; + + openDeleteModal(certId, deviceType, certType) { + this.setState({ + showDeleteModal: true, + deleteTarget: { id: certId, deviceType, certType }, + deleteError: null, + }); + } + + async handleDeleteConfirm() { + const { deleteTarget } = this.state; + if (!deleteTarget) { + return; + } + + clearTimeout(this._deleteErrorTimer); + this.setState({ isDeleting: true, deleteError: null }); + + try { + if (deleteTarget.certType === 'p8') { + await this.context.deleteP8AuthKey(deleteTarget.id, deleteTarget.deviceType); + } else { + await this.context.deleteP12Certificate(deleteTarget.id, deleteTarget.deviceType); + } + this.setState({ + isDeleting: false, + showDeleteModal: false, + deleteTarget: null, + isLoading: true, + }); + this.loadConfig(); + } catch (err) { + const msg = getErrorMessage(err, 'Failed to delete certificate.'); + this.setState({ isDeleting: false, showDeleteModal: false, deleteError: msg }); + this._deleteErrorTimer = setTimeout(() => { + if (this._isMounted) { + this.setState({ deleteError: null }); + } + }, 5000); + } + } + + renderP8Table() { + const { p8Certificates, hasPermission } = this.state; + + if (p8Certificates.length === 0) { + return
No authentication keys configured.
; + } + + return ( + + + + + + + + + + + + + {p8Certificates.map(cert => ( + + + + + + + + + ))} + +
TopicKey IDTeam IDProductionDevice Type
{cert.topic}{cert.keyId}{cert.teamId}{cert.production ? 'true' : 'false'}{cert.deviceType} + +
+ ); + } + + renderP12Table() { + const { p12Certificates, hasPermission } = this.state; + + if (p12Certificates.length === 0) { + return
No certificates configured.
; + } + + return ( + + + + + + + + + + + + {p12Certificates.map(cert => ( + + + + + + + + ))} + +
Bundle IDProductionExpirationDevice Type
{cert.bundleId}{cert.production ? 'Production' : 'Development'}{formatDate(cert.expiresAt)}{cert.deviceType} + +
+ ); + } + + renderDeleteModal() { + if (!this.state.showDeleteModal) { + return null; + } + const label = this.state.deleteTarget?.certType === 'p8' + ? 'authentication key' : 'certificate'; + return ( + this.setState({ showDeleteModal: false, deleteTarget: null })} + onConfirm={this.handleDeleteConfirm.bind(this)} + progress={this.state.isDeleting} + disabled={this.state.isDeleting} + /> + ); + } + + renderContent() { + const toolbar = ( + + { + this.setState({ isLoading: true, loadingError: null }); + this.loadConfig(); + }} + title="Refresh" + > + + + + ); + const { + isLoading, + loadingError, + hasPermission, + incompatiblePS, + disableP12, + deviceTypes, + deleteError, + } = this.state; + + let content = null; + + if (loadingError) { + content = ( +
+ { + this.setState({ isLoading: true, loadingError: null }); + this.loadConfig(); + }} + /> +
+ ); + } else if (!isLoading) { + content = ( +
+
+
+
iOS Push Settings
+
+ Manage Apple Push Notification certificates for your iOS and macOS applications. +
+ +
+ We recommend authentication keys as they are the most current method for push notifications. +
+ +
+
+
+

APNs Authentication Key

+ {incompatiblePS ? ( + +
+ {this.renderP8Table()} +
+ +
+ +
+
+

APNs Certificates

+
+ {this.renderP12Table()} +
+
+ + {deleteError && ( +
{deleteError}
+ )} +
+
+
+ ); + } + + return ( +
+ +
{content}
+
+ {toolbar} + {this.renderDeleteModal()} + {this.state.showP8Modal && ( + + )} + {this.state.showP12Modal && ( + + )} +
+ ); + } +} + +export default PushiOSSettings; diff --git a/src/dashboard/Push/PushiOSSettings.scss b/src/dashboard/Push/PushiOSSettings.scss new file mode 100644 index 000000000..5c768677a --- /dev/null +++ b/src/dashboard/Push/PushiOSSettings.scss @@ -0,0 +1,260 @@ +@import 'stylesheets/globals.scss'; +@import 'stylesheets/back4app.scss'; + +.content { + @include SoraFont; + position: relative; + min-height: calc(#{$content-max-height} - #{$toolbar-height}); + margin-top: $toolbar-height; + background: $dark; + color: $white; + & .mainContent { + height: calc(#{$content-max-height} - #{$toolbar-height}); + position: relative; + overflow: auto; + } +} + +.settingsWrapper { + width: 100%; + padding: 0; + max-width: 870px; + margin: 0 auto; + margin-top: 3rem; +} + +@media only screen and (max-width: 1200px) { + .settingsWrapper { + padding: 0 2rem; + } +} + +.settingsContainer { + .heading { + @include InterFont; + font-size: 1.125rem; + font-weight: 600; + line-height: 140%; + margin-bottom: 0.5rem; + } + + .subheading { + @include InterFont; + font-size: 0.875rem; + font-weight: 400; + line-height: 140%; + color: $light-grey; + margin-bottom: 1rem; + } + +} + +.warningBanner { + @include InterFont; + font-size: 0.875rem; + font-weight: 400; + line-height: 140%; + color: $alert-yellow; + margin-bottom: 2rem; +} + +.section { + padding: 1.25rem 0; + + &:first-child { + padding-top: 0; + } +} + +.sectionHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + + h4 { + @include InterFont; + font-size: 1rem; + font-weight: 600; + line-height: 140%; + margin: 0; + } +} + +.sectionDivider { + border: 0; + border-top: 1px solid rgba(255, 255, 255, 0.08); + margin: 0.5rem 0; +} + +.certTable { + width: 100%; + border-collapse: collapse; + @include InterFont; + font-size: 0.8125rem; + + th { + text-align: left; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + color: $light-grey; + padding: 8px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + white-space: nowrap; + } + + td { + padding: 10px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + color: $white; + word-break: break-all; + vertical-align: middle; + } + + tr:last-child td { + border-bottom: none; + } +} + +.emptyMessage { + @include InterFont; + font-size: 0.875rem; + font-weight: 400; + line-height: 140%; + color: $light-grey; + padding: 0.75rem 0; +} + +.deleteButton { + background: none; + border: none; + cursor: pointer; + padding: 6px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + opacity: 0.8; + transition: opacity 0.15s ease; + flex-shrink: 0; + + &:hover { + opacity: 1; + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } +} + +@keyframes slideDown { + from { + max-height: 0; + padding-top: 0; + padding-bottom: 0; + opacity: 0; + } + to { + max-height: 100px; + padding-top: 6px; + padding-bottom: 10px; + opacity: 1; + } +} + +.deleteError { + background: $error-red; + font-size: 0.8rem; + text-align: center; + color: $white; + padding: 6px 16px 10px; + overflow: hidden; + animation: slideDown 0.3s ease-out forwards; + margin-top: 0.5rem; + border-radius: 4px; +} + +.noPermission { + pointer-events: none; + opacity: 0.6; +} + +.fileInputField { + padding: 0 1rem; + width: 100%; + + > div { + height: auto; + text-align: left; + white-space: normal; + overflow: visible; + + > div { + float: none !important; + margin-top: 0; + margin-right: 0; + } + + > span, + > a { + display: block; + float: none !important; + margin-top: 4px; + font-size: 0.75rem; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + } + } +} + +.radiobuttonWrapper { + display: flex; + justify-content: flex-start; + align-items: center; + padding: 0 1rem; + gap: 1.25rem; + box-sizing: border-box; +} + +.radioOption { + display: flex; + align-items: center; + gap: 0.25rem; + cursor: pointer; + white-space: nowrap; + font-size: 0.875rem; +} + +.toolbarButton { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + opacity: 0.8; + transition: opacity 0.15s ease; + + &:hover { + opacity: 1; + } + + svg { + fill: $white; + } +} + +.tooltipWrapper { + display: inline-block; + cursor: help; +} + +.addButtonLabel { + display: flex; + gap: 0.5rem; + align-items: center; +} diff --git a/src/lib/ParseApp.js b/src/lib/ParseApp.js index 6ff34cfa8..4b8ab1e75 100644 --- a/src/lib/ParseApp.js +++ b/src/lib/ParseApp.js @@ -2127,4 +2127,87 @@ export default class ParseApp { throw err.response && err.response.data && err.response.data.error ? err.response.data.error : err; } } + + async getPushIOSConfig() { + try { + return ( + await axios.get( + // eslint-disable-next-line no-undef + `${b4aSettings.BACK4APP_API_PATH}/parse-app/${this.slug}/push/list`, + { withCredentials: true } + ) + ).data; + } catch (err) { + throw err.response && err.response.data && err.response.data.error ? err.response.data.error : err; + } + } + + async uploadP8AuthKey(file, keyId, teamId, bundleId, deviceType, production) { + try { + const formData = new FormData(); + formData.append('certificate', file); + formData.append('keyId', keyId); + formData.append('teamId', teamId); + formData.append('bundleId', bundleId); + formData.append('deviceType', deviceType); + formData.append('production', production); + return ( + await axios.post( + // eslint-disable-next-line no-undef + `${b4aSettings.BACK4APP_API_PATH}/parse-app/${this.slug}/push/auth`, + formData, + { withCredentials: true } + ) + ).data; + } catch (err) { + throw err.response && err.response.data && err.response.data.error ? err.response.data.error : err; + } + } + + async uploadP12Certificate(file, deviceType, production) { + try { + const formData = new FormData(); + formData.append('certificate', file); + formData.append('deviceType', deviceType); + formData.append('production', production); + return ( + await axios.post( + // eslint-disable-next-line no-undef + `${b4aSettings.BACK4APP_API_PATH}/parse-app/${this.slug}/push/cert`, + formData, + { withCredentials: true } + ) + ).data; + } catch (err) { + throw err.response && err.response.data && err.response.data.error ? err.response.data.error : err; + } + } + + async deleteP8AuthKey(certificateId, dataType) { + try { + return ( + await axios.delete( + // eslint-disable-next-line no-undef + `${b4aSettings.BACK4APP_API_PATH}/parse-app/${this.slug}/push/authkey`, + { data: { certificateId, dataType }, withCredentials: true } + ) + ).data; + } catch (err) { + throw err.response && err.response.data && err.response.data.error ? err.response.data.error : err; + } + } + + async deleteP12Certificate(certificateId, dataType) { + try { + return ( + await axios.delete( + // eslint-disable-next-line no-undef + `${b4aSettings.BACK4APP_API_PATH}/parse-app/${this.slug}/push/ios`, + { data: { certificateId, dataType }, withCredentials: true } + ) + ).data; + } catch (err) { + throw err.response && err.response.data && err.response.data.error ? err.response.data.error : err; + } + } } From 790935af7ee36afec1f77e61c41e80dc5826c3d3 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 16:33:11 +0530 Subject: [PATCH 02/17] update message --- src/dashboard/Push/PushiOSSettings.react.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dashboard/Push/PushiOSSettings.react.js b/src/dashboard/Push/PushiOSSettings.react.js index 78788e9ae..404cb76f4 100644 --- a/src/dashboard/Push/PushiOSSettings.react.js +++ b/src/dashboard/Push/PushiOSSettings.react.js @@ -113,7 +113,7 @@ class PushiOSSettings extends DashboardView { loadingError: null, p8Certificates, p12Certificates, - deviceTypes: data.deviceTypes || [], + deviceTypes: [...new Set([...(data.deviceTypes || []), 'tvos'])], disableP12: !!data.disableP12, incompatiblePS: !!data.isCompatiblePS, hasPermission, @@ -349,7 +349,7 @@ class PushiOSSettings extends DashboardView {
- We recommend authentication keys as they are the most current method for push notifications. + We recommend using APNs Authentication Keys (.p8) as they are the most current and reliable method for sending push notifications.
From d3db1dcd234cd63de9cc309317ba293ffff83fa9 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 16:48:11 +0530 Subject: [PATCH 03/17] fix delete apis --- src/lib/ParseApp.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/ParseApp.js b/src/lib/ParseApp.js index 4b8ab1e75..7a64a239d 100644 --- a/src/lib/ParseApp.js +++ b/src/lib/ParseApp.js @@ -2189,7 +2189,7 @@ export default class ParseApp { await axios.delete( // eslint-disable-next-line no-undef `${b4aSettings.BACK4APP_API_PATH}/parse-app/${this.slug}/push/authkey`, - { data: { certificateId, dataType }, withCredentials: true } + { data: JSON.stringify({ certificateId, dataType }), headers: { 'Content-Type': 'text/plain' }, withCredentials: true } ) ).data; } catch (err) { @@ -2203,7 +2203,7 @@ export default class ParseApp { await axios.delete( // eslint-disable-next-line no-undef `${b4aSettings.BACK4APP_API_PATH}/parse-app/${this.slug}/push/ios`, - { data: { certificateId, dataType }, withCredentials: true } + { data: JSON.stringify({ certificateId, dataType }), headers: { 'Content-Type': 'text/plain' }, withCredentials: true } ) ).data; } catch (err) { From 39a19932e0f256df5044851937077cb435486162 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 16:48:30 +0530 Subject: [PATCH 04/17] fixing minor bugs --- src/dashboard/Push/P12CertificateModal.react.js | 2 ++ src/dashboard/Push/P8AuthKeyModal.react.js | 2 ++ src/dashboard/Push/PushiOSSettings.react.js | 9 +++++---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/dashboard/Push/P12CertificateModal.react.js b/src/dashboard/Push/P12CertificateModal.react.js index cec63c1c8..b80562d7f 100644 --- a/src/dashboard/Push/P12CertificateModal.react.js +++ b/src/dashboard/Push/P12CertificateModal.react.js @@ -46,6 +46,8 @@ const P12CertificateModal = ({ deviceTypes, onSave, onClose }) => { setServerError(null); try { await onSave(file, deviceType, production); + setSaving(false); + onClose(); } catch (err) { const msg = typeof err === 'string' ? err : (err && err.error) || (err && err.message) || 'Failed to save certificate.'; diff --git a/src/dashboard/Push/P8AuthKeyModal.react.js b/src/dashboard/Push/P8AuthKeyModal.react.js index 7d9d7ef9d..c1213372d 100644 --- a/src/dashboard/Push/P8AuthKeyModal.react.js +++ b/src/dashboard/Push/P8AuthKeyModal.react.js @@ -66,6 +66,8 @@ const P8AuthKeyModal = ({ deviceTypes, onSave, onClose }) => { setServerError(null); try { await onSave(file, keyId.trim(), teamId.trim(), bundleId.trim(), deviceType, production); + setSaving(false); + onClose(); } catch (err) { const msg = typeof err === 'string' ? err : (err && err.error) || (err && err.message) || 'Failed to save authentication key.'; diff --git a/src/dashboard/Push/PushiOSSettings.react.js b/src/dashboard/Push/PushiOSSettings.react.js index 404cb76f4..e1db9fdac 100644 --- a/src/dashboard/Push/PushiOSSettings.react.js +++ b/src/dashboard/Push/PushiOSSettings.react.js @@ -115,7 +115,7 @@ class PushiOSSettings extends DashboardView { p12Certificates, deviceTypes: [...new Set([...(data.deviceTypes || []), 'tvos'])], disableP12: !!data.disableP12, - incompatiblePS: !!data.isCompatiblePS, + incompatiblePS: !data.isCompatiblePS, hasPermission, }); } catch (err) { @@ -130,13 +130,13 @@ class PushiOSSettings extends DashboardView { handleSaveP8 = async (file, keyId, teamId, bundleId, deviceType, production) => { await this.context.uploadP8AuthKey(file, keyId, teamId, bundleId, deviceType, production); - this.setState({ showP8Modal: false, isLoading: true }); + this.setState({ isLoading: true }); this.loadConfig(); }; handleSaveP12 = async (file, deviceType, production) => { await this.context.uploadP12Certificate(file, deviceType, production); - this.setState({ showP12Modal: false, isLoading: true }); + this.setState({ isLoading: true }); this.loadConfig(); }; @@ -376,6 +376,7 @@ class PushiOSSettings extends DashboardView { primary={true} color="green" width="auto" + disabled={!hasPermission} onClick={() => this.setState({ showP8Modal: true })} value={ @@ -398,7 +399,7 @@ class PushiOSSettings extends DashboardView { primary={true} color="green" width="auto" - disabled={disableP12} + disabled={disableP12 || !hasPermission} onClick={() => this.setState({ showP12Modal: true })} value={ From 11944dfb2c35710d9af1136f59cc4ff5a36f573b Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 16:57:03 +0530 Subject: [PATCH 05/17] change texts --- src/dashboard/Push/PushiOSSettings.react.js | 6 +++--- src/dashboard/Push/PushiOSSettings.scss | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dashboard/Push/PushiOSSettings.react.js b/src/dashboard/Push/PushiOSSettings.react.js index e1db9fdac..03b49e364 100644 --- a/src/dashboard/Push/PushiOSSettings.react.js +++ b/src/dashboard/Push/PushiOSSettings.react.js @@ -343,13 +343,13 @@ class PushiOSSettings extends DashboardView {
-
iOS Push Settings
+
Apple Push Settings
- Manage Apple Push Notification certificates for your iOS and macOS applications. + Manage Apple Push Notification certificates.
- We recommend using APNs Authentication Keys (.p8) as they are the most current and reliable method for sending push notifications. + We recommend using APNs Authentication Keys (.p8) as they are the most current and reliable method for sending push notifications.
diff --git a/src/dashboard/Push/PushiOSSettings.scss b/src/dashboard/Push/PushiOSSettings.scss index 5c768677a..bbcd7a3e9 100644 --- a/src/dashboard/Push/PushiOSSettings.scss +++ b/src/dashboard/Push/PushiOSSettings.scss @@ -44,7 +44,7 @@ font-weight: 400; line-height: 140%; color: $light-grey; - margin-bottom: 1rem; + margin-bottom: 0.5rem; } } @@ -54,8 +54,8 @@ font-size: 0.875rem; font-weight: 400; line-height: 140%; - color: $alert-yellow; - margin-bottom: 2rem; + color: $light-grey; + margin-bottom: 3rem; } .section { From 67ed6bacb394d2319d762efbf483c8215ca09770 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 16:58:02 +0530 Subject: [PATCH 06/17] limitt to 10 characters only --- src/dashboard/Push/P8AuthKeyModal.react.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dashboard/Push/P8AuthKeyModal.react.js b/src/dashboard/Push/P8AuthKeyModal.react.js index c1213372d..8a7e4cd1f 100644 --- a/src/dashboard/Push/P8AuthKeyModal.react.js +++ b/src/dashboard/Push/P8AuthKeyModal.react.js @@ -110,7 +110,7 @@ const P8AuthKeyModal = ({ deviceTypes, onSave, onClose }) => { dark={false} placeholder="Insert your Key ID" value={keyId} - onChange={value => { setKeyId(value); setErrors(prev => ({ ...prev, keyId: undefined })); }} + onChange={value => { if (value.length <= 10) { setKeyId(value); setErrors(prev => ({ ...prev, keyId: undefined })); } }} /> } /> @@ -122,7 +122,7 @@ const P8AuthKeyModal = ({ deviceTypes, onSave, onClose }) => { dark={false} placeholder="Insert your Team ID" value={teamId} - onChange={value => { setTeamId(value); setErrors(prev => ({ ...prev, teamId: undefined })); }} + onChange={value => { if (value.length <= 10) { setTeamId(value); setErrors(prev => ({ ...prev, teamId: undefined })); } }} /> } /> From 4a89cf1a9c4959b8d17ba6a6d4a16ad3fbd18b88 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 17:01:25 +0530 Subject: [PATCH 07/17] fix bugs --- src/dashboard/Push/P8AuthKeyModal.react.js | 13 +++++++++++-- src/dashboard/Push/PushiOSSettings.react.js | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/dashboard/Push/P8AuthKeyModal.react.js b/src/dashboard/Push/P8AuthKeyModal.react.js index 8a7e4cd1f..be0278149 100644 --- a/src/dashboard/Push/P8AuthKeyModal.react.js +++ b/src/dashboard/Push/P8AuthKeyModal.react.js @@ -42,7 +42,7 @@ function validate(fields) { return errors; } -const P8AuthKeyModal = ({ deviceTypes, onSave, onClose }) => { +const P8AuthKeyModal = ({ deviceTypes, hasP12Certificates, onSave, onClose }) => { const [file, setFile] = useState(null); const [keyId, setKeyId] = useState(''); const [teamId, setTeamId] = useState(''); @@ -62,6 +62,15 @@ const P8AuthKeyModal = ({ deviceTypes, onSave, onClose }) => { return; } + if (hasP12Certificates) { + const confirmed = window.confirm( + 'Are you sure? By saving your authentication keys, your p12 certificates will be removed.' + ); + if (!confirmed) { + return; + } + } + setSaving(true); setServerError(null); try { @@ -74,7 +83,7 @@ const P8AuthKeyModal = ({ deviceTypes, onSave, onClose }) => { setServerError(msg); setSaving(false); } - }, [file, keyId, teamId, bundleId, deviceType, production, onSave]); + }, [file, keyId, teamId, bundleId, deviceType, production, hasP12Certificates, onSave]); const firstError = Object.values(errors).find(Boolean); diff --git a/src/dashboard/Push/PushiOSSettings.react.js b/src/dashboard/Push/PushiOSSettings.react.js index 03b49e364..d7a25c3b0 100644 --- a/src/dashboard/Push/PushiOSSettings.react.js +++ b/src/dashboard/Push/PushiOSSettings.react.js @@ -115,7 +115,7 @@ class PushiOSSettings extends DashboardView { p12Certificates, deviceTypes: [...new Set([...(data.deviceTypes || []), 'tvos'])], disableP12: !!data.disableP12, - incompatiblePS: !data.isCompatiblePS, + incompatiblePS: !!data.isCompatiblePS, hasPermission, }); } catch (err) { @@ -432,6 +432,7 @@ class PushiOSSettings extends DashboardView { {this.state.showP8Modal && ( 0} onSave={this.handleSaveP8} onClose={this.handleCloseP8Modal} /> From 1623269519c7034d08e52907abaf97e9de955782 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 17:06:50 +0530 Subject: [PATCH 08/17] add amplitude events --- .../Notification/EmailVerification.react.js | 2 ++ src/dashboard/Push/PushAndroidSettings.react.js | 2 ++ src/dashboard/Push/PushiOSSettings.react.js | 3 +++ src/dashboard/Settings/SocialAuth.react.js | 13 ++++++++++++- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/dashboard/Notification/EmailVerification.react.js b/src/dashboard/Notification/EmailVerification.react.js index 662f040b7..77e3a9ec1 100644 --- a/src/dashboard/Notification/EmailVerification.react.js +++ b/src/dashboard/Notification/EmailVerification.react.js @@ -17,6 +17,7 @@ import B4aModal from 'components/B4aModal/B4aModal.react'; import Button from 'components/Button/Button.react'; import { Link } from 'react-router-dom'; import validateEmailFormat from 'lib/validateEmailFormat'; +import { amplitudeLogEvent } from 'lib/amplitudeEvents'; const DEFAULT_VERIFICATION_BODY = 'Hi,\n\n' + @@ -553,6 +554,7 @@ class EmailVerification extends DashboardView { return this.context.updateEmailSettings(emailSettings, preventLoginWithUnverifiedEmail); }} afterSave={({ fields, resetFields }) => { + amplitudeLogEvent('Verification email configured'); this.setState({ initialFields: { ...fields }, isDirty: false, diff --git a/src/dashboard/Push/PushAndroidSettings.react.js b/src/dashboard/Push/PushAndroidSettings.react.js index 77634385d..1ca602dfc 100644 --- a/src/dashboard/Push/PushAndroidSettings.react.js +++ b/src/dashboard/Push/PushAndroidSettings.react.js @@ -12,6 +12,7 @@ import FileInput from 'components/FileInput/FileInput.react'; import Icon from 'components/Icon/Icon.react'; import B4aModal from 'components/B4aModal/B4aModal.react'; import EmptyGhostState from 'components/EmptyGhostState/EmptyGhostState.react'; +import { amplitudeLogEvent } from 'lib/amplitudeEvents'; import styles from './PushAndroidSettings.scss'; const getLoadErrorMessage = (err, fallbackMessage) => { @@ -398,6 +399,7 @@ class PushAndroidSettings extends DashboardView { }); }} afterSave={({ resetFields }) => { + amplitudeLogEvent('Configured Android Push'); resetFields(); this.setState({ selectedFile: null, fileError: null }); this.loadConfig(); diff --git a/src/dashboard/Push/PushiOSSettings.react.js b/src/dashboard/Push/PushiOSSettings.react.js index d7a25c3b0..9321de85a 100644 --- a/src/dashboard/Push/PushiOSSettings.react.js +++ b/src/dashboard/Push/PushiOSSettings.react.js @@ -9,6 +9,7 @@ import Icon from 'components/Icon/Icon.react'; import EmptyGhostState from 'components/EmptyGhostState/EmptyGhostState.react'; import P8AuthKeyModal from './P8AuthKeyModal.react'; import P12CertificateModal from './P12CertificateModal.react'; +import { amplitudeLogEvent } from 'lib/amplitudeEvents'; import styles from './PushiOSSettings.scss'; const getErrorMessage = (err, fallback) => { @@ -130,12 +131,14 @@ class PushiOSSettings extends DashboardView { handleSaveP8 = async (file, keyId, teamId, bundleId, deviceType, production) => { await this.context.uploadP8AuthKey(file, keyId, teamId, bundleId, deviceType, production); + amplitudeLogEvent('Configured iOS Push'); this.setState({ isLoading: true }); this.loadConfig(); }; handleSaveP12 = async (file, deviceType, production) => { await this.context.uploadP12Certificate(file, deviceType, production); + amplitudeLogEvent('Configured iOS Push'); this.setState({ isLoading: true }); this.loadConfig(); }; diff --git a/src/dashboard/Settings/SocialAuth.react.js b/src/dashboard/Settings/SocialAuth.react.js index ed1388b8a..2953ccfd4 100644 --- a/src/dashboard/Settings/SocialAuth.react.js +++ b/src/dashboard/Settings/SocialAuth.react.js @@ -8,6 +8,7 @@ import FlowView from 'components/FlowView/FlowView.react'; import layoutStyles from '../CustomParseOptions/CustomParseOptions.scss'; import fbStyles from './SocialAuth.scss'; import EmptyGhostState from 'components/EmptyGhostState/EmptyGhostState.react'; +import { amplitudeLogEvent } from 'lib/amplitudeEvents'; import Label from 'components/Label/Label.react'; import Field from 'components/Field/Field.react'; @@ -682,7 +683,17 @@ class SocialAuth extends DashboardView { const payload = buildOauthPayload(oauth); return this.context.updateOauth(payload); }} - afterSave={({ resetFields }) => { + afterSave={({ fields, resetFields }) => { + const prevOauth = this.state.initialFields.oauth || {}; + const currOauth = fields.oauth || {}; + const prevPayload = buildOauthPayload(prevOauth); + const currPayload = buildOauthPayload(currOauth); + if (currPayload.facebook && JSON.stringify(currPayload.facebook) !== JSON.stringify(prevPayload.facebook)) { + amplitudeLogEvent('facebook login configured'); + } + if (currPayload.twitter && JSON.stringify(currPayload.twitter) !== JSON.stringify(prevPayload.twitter)) { + amplitudeLogEvent('twitter login configured'); + } this.loadData(); this._lastComputedDirty = false; this.setState({ isDirty: false }); From a82fbdd46695ebcda198aebb1760a49d374d0736 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 17:15:57 +0530 Subject: [PATCH 09/17] prevent closing --- src/dashboard/Push/P12CertificateModal.react.js | 1 + src/dashboard/Push/P8AuthKeyModal.react.js | 1 + src/dashboard/Push/PushiOSSettings.react.js | 1 + 3 files changed, 3 insertions(+) diff --git a/src/dashboard/Push/P12CertificateModal.react.js b/src/dashboard/Push/P12CertificateModal.react.js index b80562d7f..c5071c1e4 100644 --- a/src/dashboard/Push/P12CertificateModal.react.js +++ b/src/dashboard/Push/P12CertificateModal.react.js @@ -69,6 +69,7 @@ const P12CertificateModal = ({ deviceTypes, onSave, onClose }) => { onCancel={onClose} disabled={saving} progress={saving} + canCancel={!saving} > } diff --git a/src/dashboard/Push/P8AuthKeyModal.react.js b/src/dashboard/Push/P8AuthKeyModal.react.js index be0278149..03e6db4fa 100644 --- a/src/dashboard/Push/P8AuthKeyModal.react.js +++ b/src/dashboard/Push/P8AuthKeyModal.react.js @@ -98,6 +98,7 @@ const P8AuthKeyModal = ({ deviceTypes, hasP12Certificates, onSave, onClose }) => onCancel={onClose} disabled={saving} progress={saving} + canCancel={!saving} > } diff --git a/src/dashboard/Push/PushiOSSettings.react.js b/src/dashboard/Push/PushiOSSettings.react.js index 9321de85a..f970b812d 100644 --- a/src/dashboard/Push/PushiOSSettings.react.js +++ b/src/dashboard/Push/PushiOSSettings.react.js @@ -296,6 +296,7 @@ class PushiOSSettings extends DashboardView { onConfirm={this.handleDeleteConfirm.bind(this)} progress={this.state.isDeleting} disabled={this.state.isDeleting} + canCancel={!this.state.isDeleting} /> ); } From 9acb8e4a358a27053533710f0e460a0f68a4f9bd Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 17:25:25 +0530 Subject: [PATCH 10/17] fix icon close --- src/components/B4aModal/B4aModal.react.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/B4aModal/B4aModal.react.js b/src/components/B4aModal/B4aModal.react.js index f423b6000..6a6e11690 100644 --- a/src/components/B4aModal/B4aModal.react.js +++ b/src/components/B4aModal/B4aModal.react.js @@ -92,7 +92,14 @@ const B4aModal = ({ return (
- {showCancel && } + {showCancel && ( + + + + )}
{iconNode ? (
From 283eb4adcc37acabe715f25f5560db4b1fece511 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Wed, 15 Apr 2026 17:50:59 +0530 Subject: [PATCH 11/17] fix access thing --- src/dashboard/DashboardView.react.js | 6 +++++- src/lib/serverInfo.js | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js index 184063432..c875a28f2 100644 --- a/src/dashboard/DashboardView.react.js +++ b/src/dashboard/DashboardView.react.js @@ -83,7 +83,11 @@ export default class DashboardView extends React.Component { } render() { - const isLocked = !canAccess(this.context.serverInfo, window.location.pathname.split('/')[3]); + const pathSegments = window.location.pathname.split('/'); + const topRoute = pathSegments[3]; + const fullSubRoute = pathSegments.slice(3, 5).join('/'); + const isLocked = !canAccess(this.context.serverInfo, topRoute) + && !canAccess(this.context.serverInfo, fullSubRoute); if (isLocked) { return (
diff --git a/src/lib/serverInfo.js b/src/lib/serverInfo.js index 81cc2f983..752b4d964 100644 --- a/src/lib/serverInfo.js +++ b/src/lib/serverInfo.js @@ -28,7 +28,8 @@ export const ALWAYS_ALLOWED_ROUTES = [ 'Social Auth', 'Notification', 'notification', - 'push', + 'push/android-settings', + 'push/ios-settings', 'server-url-live-query', 'Server URL & Live Query', ]; From a2359c122a16c5e96f24d811a77a751621daf451 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Thu, 16 Apr 2026 13:43:12 +0530 Subject: [PATCH 12/17] requested changes --- src/components/Sidebar/B4aSidebar.react.js | 3 + .../Sidebar/SidebarSubItem.react.js | 13 +- src/dashboard/DashboardView.react.js | 9 +- .../Notification/EmailPasswordReset.react.js | 4 +- .../Notification/EmailVerification.react.js | 4 +- src/dashboard/Push/P8AuthKeyModal.react.js | 56 ++++++--- .../Push/PushAndroidSettings.react.js | 22 +++- .../Push/PushAudiencesIndex.react.js | 2 +- src/dashboard/Push/PushDetails.react.js | 2 +- src/dashboard/Push/PushIndex.react.js | 4 +- src/dashboard/Push/PushNew.react.js | 2 +- src/dashboard/Push/PushiOSSettings.react.js | 116 +++++++++++++----- src/lib/serverInfo.js | 4 + 13 files changed, 179 insertions(+), 62 deletions(-) diff --git a/src/components/Sidebar/B4aSidebar.react.js b/src/components/Sidebar/B4aSidebar.react.js index 4731c3466..0e29eb772 100644 --- a/src/components/Sidebar/B4aSidebar.react.js +++ b/src/components/Sidebar/B4aSidebar.react.js @@ -135,6 +135,8 @@ const B4aSidebar = ({ {groupChildren.map(child => { const childActive = subsection === child.name; const childLink = child.link.startsWith('/') ? prefix + child.link : child.link; + const childRoute = child.link.replace(/^\//, ''); + const childDisabled = !canAccess(currentApp.serverInfo, childRoute); return ( {childActive ? children : null} diff --git a/src/components/Sidebar/SidebarSubItem.react.js b/src/components/Sidebar/SidebarSubItem.react.js index 8757eb7ea..18d5dedd1 100644 --- a/src/components/Sidebar/SidebarSubItem.react.js +++ b/src/components/Sidebar/SidebarSubItem.react.js @@ -16,7 +16,18 @@ const sendEvent = () => { back4AppNavigation.atApiReferenceIntroEvent(); }; -let SidebarSubItem = ({ active, name, action, link, children, badge }) => { +let SidebarSubItem = ({ active, name, action, link, children, badge, disabled }) => { + if (disabled) { + return ( +
+ + {name} + {badge ? : null} + +
+ ); + } + if (active) { return (
diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js index c875a28f2..76b2c3a5b 100644 --- a/src/dashboard/DashboardView.react.js +++ b/src/dashboard/DashboardView.react.js @@ -353,20 +353,19 @@ export default class DashboardView extends React.Component { ], }, { - name: 'Pushes', + name: 'Push', link: '/push/new', children: [ { name: 'Send New Push', link: '/push/new' }, - { name: 'Past Pushes', link: '/push/activity' }, + { name: 'History', link: '/push/activity' }, { name: 'Audiences', link: '/push/audiences' }, - { name: 'Android', link: '/push/android-settings' }, - { name: 'iOS', link: '/push/ios-settings' }, + { name: 'Setup', link: '/push/android-settings' }, ], }, ]; appSidebarSections.push({ - name: 'Notification', + name: 'Notifications', icon: 'b4a-push-notification-icon', link: '/notification', subsections: notificationSubSections, diff --git a/src/dashboard/Notification/EmailPasswordReset.react.js b/src/dashboard/Notification/EmailPasswordReset.react.js index ad5c39827..6e7b956a0 100644 --- a/src/dashboard/Notification/EmailPasswordReset.react.js +++ b/src/dashboard/Notification/EmailPasswordReset.react.js @@ -91,7 +91,7 @@ const renderChangedValuesFooter = (changes, fieldOptions) => { class EmailPasswordReset extends DashboardView { constructor() { super(); - this.section = 'Notification'; + this.section = 'Notifications'; this.subsection = 'Password Reset'; this.state = { isLoading: true, @@ -345,7 +345,7 @@ class EmailPasswordReset extends DashboardView { renderContent() { const toolbar = ( - + ); const { isLoading, initialFields, errorMessage, hasPermission, isUserVerified } = this.state; diff --git a/src/dashboard/Notification/EmailVerification.react.js b/src/dashboard/Notification/EmailVerification.react.js index 77e3a9ec1..c3a06cc6c 100644 --- a/src/dashboard/Notification/EmailVerification.react.js +++ b/src/dashboard/Notification/EmailVerification.react.js @@ -100,7 +100,7 @@ const renderChangedValuesFooter = (changes, fieldOptions) => { class EmailVerification extends DashboardView { constructor() { super(); - this.section = 'Notification'; + this.section = 'Notifications'; this.subsection = 'Verification'; this.state = { isLoading: true, @@ -484,7 +484,7 @@ class EmailVerification extends DashboardView { renderContent() { const toolbar = ( - + ); const { isLoading, initialFields, errorMessage, hasPermission, isUserVerified, canChangeEmailTemplate } = this.state; diff --git a/src/dashboard/Push/P8AuthKeyModal.react.js b/src/dashboard/Push/P8AuthKeyModal.react.js index 03e6db4fa..e7670e0fe 100644 --- a/src/dashboard/Push/P8AuthKeyModal.react.js +++ b/src/dashboard/Push/P8AuthKeyModal.react.js @@ -115,25 +115,53 @@ const P8AuthKeyModal = ({ deviceTypes, hasP12Certificates, onSave, onClose }) => } input={ - { if (value.length <= 10) { setKeyId(value); setErrors(prev => ({ ...prev, keyId: undefined })); } }} - /> +
+ { + if (value.length <= 10) { + setKeyId(value); + if (value.trim().length === 10) { + setErrors(prev => ({ ...prev, keyId: undefined })); + } else if (value.trim().length > 0) { + setErrors(prev => ({ ...prev, keyId: 'Key ID must be exactly 10 characters.' })); + } else { + setErrors(prev => ({ ...prev, keyId: undefined })); + } + } + }} + /> + {errors.keyId && *{errors.keyId}} +
} /> } input={ - { if (value.length <= 10) { setTeamId(value); setErrors(prev => ({ ...prev, teamId: undefined })); } }} - /> +
+ { + if (value.length <= 10) { + setTeamId(value); + if (value.trim().length === 10) { + setErrors(prev => ({ ...prev, teamId: undefined })); + } else if (value.trim().length > 0) { + setErrors(prev => ({ ...prev, teamId: 'Team ID must be exactly 10 characters.' })); + } else { + setErrors(prev => ({ ...prev, teamId: undefined })); + } + } + }} + /> + {errors.teamId && *{errors.teamId}} +
} /> { class PushAndroidSettings extends DashboardView { constructor() { super(); - this.section = 'Notification'; - this.subsection = 'Android'; + this.section = 'Notifications'; + this.subsection = 'Setup'; this.state = { isLoading: true, loadingError: null, @@ -330,8 +331,23 @@ class PushAndroidSettings extends DashboardView { ); } + renderSidebar() { + const { pathname } = this.props.location; + const current = pathname.substr(pathname.lastIndexOf('/') + 1, pathname.length - 1); + return ( + + ); + } + renderContent() { - const toolbar = ; + const toolbar = ; const { isLoading, loadingError, hasPermission, selectedFile } = this.state; let content = null; diff --git a/src/dashboard/Push/PushAudiencesIndex.react.js b/src/dashboard/Push/PushAudiencesIndex.react.js index e0969e153..ae822decb 100644 --- a/src/dashboard/Push/PushAudiencesIndex.react.js +++ b/src/dashboard/Push/PushAudiencesIndex.react.js @@ -38,7 +38,7 @@ const XHR_KEY = 'PushAudiencesIndex'; class PushAudiencesIndex extends DashboardView { constructor() { super(); - this.section = 'Notification'; + this.section = 'Notifications'; this.subsection = 'Audiences'; this.state = { availableDevices: [], diff --git a/src/dashboard/Push/PushDetails.react.js b/src/dashboard/Push/PushDetails.react.js index 51fad95f4..5247ac21e 100644 --- a/src/dashboard/Push/PushDetails.react.js +++ b/src/dashboard/Push/PushDetails.react.js @@ -231,7 +231,7 @@ const DROPDOWN_KEY_GROUP_B = 'Group B'; class PushDetails extends DashboardView { constructor() { super(); - this.section = 'Notification'; + this.section = 'Notifications'; this.subsection = ''; this.state = { pushDetails: {}, diff --git a/src/dashboard/Push/PushIndex.react.js b/src/dashboard/Push/PushIndex.react.js index 9e6d5d231..2f587c22c 100644 --- a/src/dashboard/Push/PushIndex.react.js +++ b/src/dashboard/Push/PushIndex.react.js @@ -228,8 +228,8 @@ const getPushTime = (pushTime, updatedAt) => { class PushIndex extends DashboardView { constructor() { super(); - this.section = 'Notification'; - this.subsection = 'Past Pushes'; + this.section = 'Notifications'; + this.subsection = 'History'; this.action = new SidebarAction('Send a push', this.navigateToNew.bind(this)); this.state = { pushes: [], diff --git a/src/dashboard/Push/PushNew.react.js b/src/dashboard/Push/PushNew.react.js index c75571917..e89006727 100644 --- a/src/dashboard/Push/PushNew.react.js +++ b/src/dashboard/Push/PushNew.react.js @@ -156,7 +156,7 @@ class PushNew extends DashboardView { constructor() { super(); this.xhrs = []; - this.section = 'Notification'; + this.section = 'Notifications'; this.subsection = 'Send New Push'; this.state = { pushAudiencesFetched: false, diff --git a/src/dashboard/Push/PushiOSSettings.react.js b/src/dashboard/Push/PushiOSSettings.react.js index f970b812d..c069b5b94 100644 --- a/src/dashboard/Push/PushiOSSettings.react.js +++ b/src/dashboard/Push/PushiOSSettings.react.js @@ -2,6 +2,7 @@ import React from 'react'; import { withRouter } from 'lib/withRouter'; import Toolbar from 'components/Toolbar/Toolbar.react'; import DashboardView from 'dashboard/DashboardView.react'; +import CategoryList from 'components/CategoryList/CategoryList.react'; import B4aLoaderContainer from 'components/B4aLoaderContainer/B4aLoaderContainer.react'; import B4aModal from 'components/B4aModal/B4aModal.react'; import Button from 'components/Button/Button.react'; @@ -9,6 +10,7 @@ import Icon from 'components/Icon/Icon.react'; import EmptyGhostState from 'components/EmptyGhostState/EmptyGhostState.react'; import P8AuthKeyModal from './P8AuthKeyModal.react'; import P12CertificateModal from './P12CertificateModal.react'; +import B4aTooltip from 'components/Tooltip/B4aTooltip.react'; import { amplitudeLogEvent } from 'lib/amplitudeEvents'; import styles from './PushiOSSettings.scss'; @@ -45,8 +47,8 @@ const formatDate = (isoString) => { class PushiOSSettings extends DashboardView { constructor() { super(); - this.section = 'Notification'; - this.subsection = 'iOS'; + this.section = 'Notifications'; + this.subsection = 'Setup'; this.state = { isLoading: true, loadingError: null, @@ -62,6 +64,8 @@ class PushiOSSettings extends DashboardView { deleteTarget: null, isDeleting: false, deleteError: null, + showP8Tooltip: false, + showP12Tooltip: false, }; } @@ -301,9 +305,24 @@ class PushiOSSettings extends DashboardView { ); } + renderSidebar() { + const { pathname } = this.props.location; + const current = pathname.substr(pathname.lastIndexOf('/') + 1, pathname.length - 1); + return ( + + ); + } + renderContent() { const toolbar = ( - + { @@ -349,7 +368,7 @@ class PushiOSSettings extends DashboardView {
Apple Push Settings
- Manage Apple Push Notification certificates. + Manage Apple Push Notification settings.
@@ -361,20 +380,30 @@ class PushiOSSettings extends DashboardView {

APNs Authentication Key

{incompatiblePS ? ( - -
{this.renderP12Table()}
diff --git a/src/lib/serverInfo.js b/src/lib/serverInfo.js index 752b4d964..688d522b3 100644 --- a/src/lib/serverInfo.js +++ b/src/lib/serverInfo.js @@ -26,8 +26,12 @@ export const ALWAYS_ALLOWED_ROUTES = [ 'environment-variables', 'social-auth', 'Social Auth', + 'Notifications', 'Notification', 'notification', + 'notification/email', + 'notification/email/verification', + 'notification/email/password-reset', 'push/android-settings', 'push/ios-settings', 'server-url-live-query', From 1804da044d68fb01aa1455f14522f733277e1607 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Thu, 16 Apr 2026 15:54:21 +0530 Subject: [PATCH 13/17] remove console.log --- src/dashboard/Dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js index df422bc98..665f00a79 100644 --- a/src/dashboard/Dashboard.js +++ b/src/dashboard/Dashboard.js @@ -172,7 +172,7 @@ const preloadMap = { const preloadRoute = async (routeName, preloadFn) => { try { await preloadFn(); - console.log(`Successfully preloaded route: ${routeName}`); + // console.log(`Successfully preloaded route: ${routeName}`); } catch (err) { console.error(`Error preloading route ${routeName}:`, err); } From e294d506dfcac94b029ec5ecad36fa706c2471e2 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Thu, 16 Apr 2026 16:02:30 +0530 Subject: [PATCH 14/17] fix form validation --- src/dashboard/Push/P8AuthKeyModal.react.js | 77 ++++++++++------------ 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/src/dashboard/Push/P8AuthKeyModal.react.js b/src/dashboard/Push/P8AuthKeyModal.react.js index e7670e0fe..b537b32e5 100644 --- a/src/dashboard/Push/P8AuthKeyModal.react.js +++ b/src/dashboard/Push/P8AuthKeyModal.react.js @@ -86,6 +86,11 @@ const P8AuthKeyModal = ({ deviceTypes, hasP12Certificates, onSave, onClose }) => }, [file, keyId, teamId, bundleId, deviceType, production, hasP12Certificates, onSave]); const firstError = Object.values(errors).find(Boolean); + const isFormValid = file && file.name.endsWith('.p8') + && keyId.trim().length === 10 + && teamId.trim().length === 10 + && bundleId.trim().length > 0 + && !!deviceType; return ( cancelText="Cancel" onConfirm={handleSubmit} onCancel={onClose} - disabled={saving} + disabled={saving || !isFormValid} progress={saving} canCancel={!saving} > @@ -115,53 +120,43 @@ const P8AuthKeyModal = ({ deviceTypes, hasP12Certificates, onSave, onClose }) => } input={ -
- { - if (value.length <= 10) { - setKeyId(value); - if (value.trim().length === 10) { - setErrors(prev => ({ ...prev, keyId: undefined })); - } else if (value.trim().length > 0) { - setErrors(prev => ({ ...prev, keyId: 'Key ID must be exactly 10 characters.' })); - } else { - setErrors(prev => ({ ...prev, keyId: undefined })); - } + { + if (value.length <= 10) { + setKeyId(value); + if (value.trim().length === 10 || value.trim().length === 0) { + setErrors(prev => ({ ...prev, keyId: undefined })); + } else { + setErrors(prev => ({ ...prev, keyId: 'Key ID must be exactly 10 characters.' })); } - }} - /> - {errors.keyId && *{errors.keyId}} -
+ } + }} + /> } /> } input={ -
- { - if (value.length <= 10) { - setTeamId(value); - if (value.trim().length === 10) { - setErrors(prev => ({ ...prev, teamId: undefined })); - } else if (value.trim().length > 0) { - setErrors(prev => ({ ...prev, teamId: 'Team ID must be exactly 10 characters.' })); - } else { - setErrors(prev => ({ ...prev, teamId: undefined })); - } + { + if (value.length <= 10) { + setTeamId(value); + if (value.trim().length === 10 || value.trim().length === 0) { + setErrors(prev => ({ ...prev, teamId: undefined })); + } else { + setErrors(prev => ({ ...prev, teamId: 'Team ID must be exactly 10 characters.' })); } - }} - /> - {errors.teamId && *{errors.teamId}} -
+ } + }} + /> } /> Date: Thu, 16 Apr 2026 16:30:10 +0530 Subject: [PATCH 15/17] fix bugs --- src/components/Sidebar/B4aSidebar.react.js | 23 ++++++++++++++++------ src/dashboard/AppData.react.js | 6 ++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/components/Sidebar/B4aSidebar.react.js b/src/components/Sidebar/B4aSidebar.react.js index 0e29eb772..d89a87204 100644 --- a/src/components/Sidebar/B4aSidebar.react.js +++ b/src/components/Sidebar/B4aSidebar.react.js @@ -123,14 +123,25 @@ const B4aSidebar = ({ if (groupChildren) { const isGroupActive = subsection === name || groupChildren.some(c => c.name === subsection); const groupLink = link.startsWith('/') ? prefix + link : link; + const groupRoute = link.replace(/^\//, ''); + const groupDisabled = !canAccess(currentApp.serverInfo, groupRoute); return (
- - {name} - + {groupDisabled ? ( + + {name} + + ) : ( + + {name} + + )}
{groupChildren.map(child => { const childActive = subsection === child.name; diff --git a/src/dashboard/AppData.react.js b/src/dashboard/AppData.react.js index 5f74daa98..305bf82e7 100644 --- a/src/dashboard/AppData.react.js +++ b/src/dashboard/AppData.react.js @@ -31,8 +31,10 @@ function AppData() { } current.setParseKeys(); - const curPathName = window.location.pathname.split('/')[3]; - if (current.serverInfo.error && !canAccess(current.serverInfo, curPathName)) { + const pathSegments = window.location.pathname.split('/'); + const curPathName = pathSegments[3]; + const fullSubRoute = pathSegments.slice(3, 5).join('/'); + if (current.serverInfo.error && !canAccess(current.serverInfo, curPathName) && !canAccess(current.serverInfo, fullSubRoute)) { navigate(`/apps/${current.slug}/overview`, { replace: true }); return
; // return ( From 07e98707eefba658f6c323dabc3f1e180f7707d4 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Thu, 16 Apr 2026 16:38:52 +0530 Subject: [PATCH 16/17] remove console.log --- src/dashboard/Dashboard.js | 2 +- src/dashboard/Data/AppOverview/AppLoadingText.react.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js index 665f00a79..d0c7c10b6 100644 --- a/src/dashboard/Dashboard.js +++ b/src/dashboard/Dashboard.js @@ -219,7 +219,7 @@ class Dashboard extends React.Component { componentDidMount() { // Start preloading routes immediately but don't block on it preloadAllRoutes().finally(() => { - console.log('Route preloading complete'); + // console.log('Route preloading complete'); }); get('/parse-dashboard-config.json').then(({ apps, newFeaturesInLatestVersion = [], user }) => { diff --git a/src/dashboard/Data/AppOverview/AppLoadingText.react.js b/src/dashboard/Data/AppOverview/AppLoadingText.react.js index b8d92b4a7..d93367d1c 100644 --- a/src/dashboard/Data/AppOverview/AppLoadingText.react.js +++ b/src/dashboard/Data/AppOverview/AppLoadingText.react.js @@ -32,7 +32,7 @@ const AppLoadingText = ({ appName, appId, pollSchemas }) => { document.documentElement.style.setProperty('--text-interval', `${TEXT_INTERVAL}ms`); document.documentElement.style.setProperty('--fill-duration', `${TEXT_INTERVAL / 2}ms`); return () => { - console.log('deleting cookie'); + // console.log('deleting cookie'); try { document.cookie = `newApp-${appId}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=back4app.com`; } catch (error) { From 400173c99156465fc3bfe9ffea0e9a021c2c7ed9 Mon Sep 17 00:00:00 2001 From: sadakchap Date: Thu, 16 Apr 2026 17:57:05 +0530 Subject: [PATCH 17/17] remove console.log --- src/dashboard/Dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js index d0c7c10b6..1afc6c031 100644 --- a/src/dashboard/Dashboard.js +++ b/src/dashboard/Dashboard.js @@ -180,7 +180,7 @@ const preloadRoute = async (routeName, preloadFn) => { // Preload all routes in parallel const preloadAllRoutes = () => { - console.log('Preloading routes...'); + // console.log('Preloading routes...'); return Promise.all( Object.entries(preloadMap).map(([routeName, preloadFn]) => preloadRoute(routeName, preloadFn)