Skip to content
9 changes: 8 additions & 1 deletion src/components/B4aModal/B4aModal.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,14 @@ const B4aModal = ({
return (
<Popover fadeIn={true} fixed={true} position={origin} modal={true} color="rgba(17,13,17,0.8)">
<div className={[styles.modal, styles[type]].join(' ')} style={{ width }}>
{showCancel && <Icon onClick={onCancel} width={10} height={10} className={styles.closeIcon} name="close" fill="#10203A" />}
{showCancel && (
<span
onClick={canCancel ? onCancel : undefined}
style={canCancel ? undefined : { pointerEvents: 'none', opacity: 0.4 }}
>
<Icon width={10} height={10} className={styles.closeIcon} name="close" fill="#10203A" />
</span>
)}
<div className={styles.header}>
{iconNode ? (
<div className={styles.icon}>
Expand Down
26 changes: 20 additions & 6 deletions src/components/Sidebar/B4aSidebar.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,31 @@ 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 (
<div key={name} className={styles.subgroup}>
<Link
className={`${styles.subgroupHeader} ${isGroupActive ? styles.subgroupHeaderActive : ''}`}
to={{ pathname: groupLink }}
>
{name}
</Link>
{groupDisabled ? (
<span
className={`${styles.subgroupHeader}`}
style={{ pointerEvents: 'none', color: '#C1E2FF', opacity: 0.4 }}
>
{name}
</span>
) : (
<Link
className={`${styles.subgroupHeader} ${isGroupActive ? styles.subgroupHeaderActive : ''}`}
to={{ pathname: groupLink }}
>
{name}
</Link>
)}
<div className={styles.subgroupChildren}>
{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 (
<SidebarSubItem
key={child.name}
Expand All @@ -144,6 +157,7 @@ const B4aSidebar = ({
actionHandler={childActive ? actionHandler : null}
active={childActive}
badge={child.badge}
disabled={childDisabled}
>
{childActive ? children : null}
</SidebarSubItem>
Expand Down
13 changes: 12 additions & 1 deletion src/components/Sidebar/SidebarSubItem.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div style={{ pointerEvents: 'none' }}>
<span className={styles.subitem} style={{ color: '#C1E2FF', opacity: 0.4 }}>
{name}
{badge ? <B4aBadge {...badge} /> : null}
</span>
</div>
);
}

if (active) {
return (
<div>
Expand Down
6 changes: 4 additions & 2 deletions src/dashboard/AppData.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div />;
// return (
Expand Down
9 changes: 6 additions & 3 deletions src/dashboard/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -164,21 +165,22 @@ 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
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);
}
};

// 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)
Expand Down Expand Up @@ -217,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 }) => {
Expand Down Expand Up @@ -507,6 +509,7 @@ class Dashboard extends React.Component {
<Route path="push/audiences" element={<PushAudiencesIndex />} />
<Route path="push/new" element={<PushNew />} />
<Route path="push/android-settings" element={<LazyComponentWrapper><LazyPushAndroidSettings /></LazyComponentWrapper>} />
<Route path="push/ios-settings" element={<LazyComponentWrapper><LazyPushiOSSettings /></LazyComponentWrapper>} />
<Route path="push/:pushId" element={<PushDetails />} />

<Route path="connect" element={<B4aConnectPage />} />
Expand Down
14 changes: 9 additions & 5 deletions src/dashboard/DashboardView.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={baseStyles.pageCenter} style={{ flexDirection: 'column' }}>
Expand Down Expand Up @@ -349,19 +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: 'Setup', link: '/push/android-settings' },
],
},
];

appSidebarSections.push({
name: 'Notification',
name: 'Notifications',
icon: 'b4a-push-notification-icon',
link: '/notification',
subsections: notificationSubSections,
Expand Down
2 changes: 1 addition & 1 deletion src/dashboard/Data/AppOverview/AppLoadingText.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/dashboard/Notification/EmailPasswordReset.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -345,7 +345,7 @@ class EmailPasswordReset extends DashboardView {

renderContent() {
const toolbar = (
<Toolbar section="Notification" subsection="Reset Password Email" />
<Toolbar section="Notifications" subsection="Reset Password Email" />
);
const { isLoading, initialFields, errorMessage, hasPermission, isUserVerified } = this.state;

Expand Down
6 changes: 4 additions & 2 deletions src/dashboard/Notification/EmailVerification.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' +
Expand Down Expand Up @@ -99,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,
Expand Down Expand Up @@ -483,7 +484,7 @@ class EmailVerification extends DashboardView {

renderContent() {
const toolbar = (
<Toolbar section="Notification" subsection="Email Verification" />
<Toolbar section="Notifications" subsection="Email Verification" />
);
const { isLoading, initialFields, errorMessage, hasPermission, isUserVerified, canChangeEmailTemplate } = this.state;

Expand Down Expand Up @@ -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,
Expand Down
137 changes: 137 additions & 0 deletions src/dashboard/Push/P12CertificateModal.react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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);
setSaving(false);
onClose();
} 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 (
<B4aModal
type={B4aModal.Types.DEFAULT}
title="APNs Certificate"
width={700}
confirmText={saving ? 'Saving\u2026' : 'Save'}
cancelText="Cancel"
onConfirm={handleSubmit}
onCancel={onClose}
disabled={saving}
progress={saving}
canCancel={!saving}
>
<Field
label={<Label text="APNs Certificate File" description="Upload your .p12 certificate file" />}
input={
<div className={styles.fileInputField}>
<FileInput
onChange={f => { setFile(f); setErrors(prev => ({ ...prev, file: undefined })); setServerError(null); }}
accept=".p12,application/x-pkcs12"
value={file ? { name: file.name } : undefined}
/>
</div>
}
/>
<Field
label={<Label text="Device Type" />}
input={
<Dropdown
value={deviceType}
onChange={value => { setDeviceType(value); setErrors(prev => ({ ...prev, deviceType: undefined })); }}
placeHolder="Select device type"
dark={false}
fixed={true}
>
{deviceTypes.map(type => (
<Option key={type} value={type}>{type}</Option>
))}
</Dropdown>
}
/>
<Field
label={<Label text="Certificate type" description="Development or production" />}
input={
<div className={styles.radiobuttonWrapper}>
<label htmlFor="p12_development" className={styles.radioOption}>
<RadioButton
dark={true}
id="p12_development"
name="p12Production"
checked={!production}
onChange={() => setProduction(false)}
/>Development
</label>
<label htmlFor="p12_production" className={styles.radioOption}>
<RadioButton
dark={true}
id="p12_production"
name="p12Production"
checked={production}
onChange={() => setProduction(true)}
/>Production
</label>
</div>
}
/>
<FormNote show={!!firstError} color="red">
{firstError}
</FormNote>
<FormNote show={!!serverError && !firstError} color="red">
{serverError}
</FormNote>
</B4aModal>
);
};

export default P12CertificateModal;
Loading
Loading