Skip to content

Commit 5dc73f8

Browse files
Merge pull request #799 from back4app/bac-1335/ios-push
add IOS push screen
2 parents 9d855d7 + 400173c commit 5dc73f8

21 files changed

Lines changed: 1327 additions & 33 deletions

src/components/B4aModal/B4aModal.react.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,14 @@ const B4aModal = ({
9292
return (
9393
<Popover fadeIn={true} fixed={true} position={origin} modal={true} color="rgba(17,13,17,0.8)">
9494
<div className={[styles.modal, styles[type]].join(' ')} style={{ width }}>
95-
{showCancel && <Icon onClick={onCancel} width={10} height={10} className={styles.closeIcon} name="close" fill="#10203A" />}
95+
{showCancel && (
96+
<span
97+
onClick={canCancel ? onCancel : undefined}
98+
style={canCancel ? undefined : { pointerEvents: 'none', opacity: 0.4 }}
99+
>
100+
<Icon width={10} height={10} className={styles.closeIcon} name="close" fill="#10203A" />
101+
</span>
102+
)}
96103
<div className={styles.header}>
97104
{iconNode ? (
98105
<div className={styles.icon}>

src/components/Sidebar/B4aSidebar.react.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,31 @@ const B4aSidebar = ({
123123
if (groupChildren) {
124124
const isGroupActive = subsection === name || groupChildren.some(c => c.name === subsection);
125125
const groupLink = link.startsWith('/') ? prefix + link : link;
126+
const groupRoute = link.replace(/^\//, '');
127+
const groupDisabled = !canAccess(currentApp.serverInfo, groupRoute);
126128
return (
127129
<div key={name} className={styles.subgroup}>
128-
<Link
129-
className={`${styles.subgroupHeader} ${isGroupActive ? styles.subgroupHeaderActive : ''}`}
130-
to={{ pathname: groupLink }}
131-
>
132-
{name}
133-
</Link>
130+
{groupDisabled ? (
131+
<span
132+
className={`${styles.subgroupHeader}`}
133+
style={{ pointerEvents: 'none', color: '#C1E2FF', opacity: 0.4 }}
134+
>
135+
{name}
136+
</span>
137+
) : (
138+
<Link
139+
className={`${styles.subgroupHeader} ${isGroupActive ? styles.subgroupHeaderActive : ''}`}
140+
to={{ pathname: groupLink }}
141+
>
142+
{name}
143+
</Link>
144+
)}
134145
<div className={styles.subgroupChildren}>
135146
{groupChildren.map(child => {
136147
const childActive = subsection === child.name;
137148
const childLink = child.link.startsWith('/') ? prefix + child.link : child.link;
149+
const childRoute = child.link.replace(/^\//, '');
150+
const childDisabled = !canAccess(currentApp.serverInfo, childRoute);
138151
return (
139152
<SidebarSubItem
140153
key={child.name}
@@ -144,6 +157,7 @@ const B4aSidebar = ({
144157
actionHandler={childActive ? actionHandler : null}
145158
active={childActive}
146159
badge={child.badge}
160+
disabled={childDisabled}
147161
>
148162
{childActive ? children : null}
149163
</SidebarSubItem>

src/components/Sidebar/SidebarSubItem.react.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,18 @@ const sendEvent = () => {
1616
back4AppNavigation.atApiReferenceIntroEvent();
1717
};
1818

19-
let SidebarSubItem = ({ active, name, action, link, children, badge }) => {
19+
let SidebarSubItem = ({ active, name, action, link, children, badge, disabled }) => {
20+
if (disabled) {
21+
return (
22+
<div style={{ pointerEvents: 'none' }}>
23+
<span className={styles.subitem} style={{ color: '#C1E2FF', opacity: 0.4 }}>
24+
{name}
25+
{badge ? <B4aBadge {...badge} /> : null}
26+
</span>
27+
</div>
28+
);
29+
}
30+
2031
if (active) {
2132
return (
2233
<div>

src/dashboard/AppData.react.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ function AppData() {
3131
}
3232

3333
current.setParseKeys();
34-
const curPathName = window.location.pathname.split('/')[3];
35-
if (current.serverInfo.error && !canAccess(current.serverInfo, curPathName)) {
34+
const pathSegments = window.location.pathname.split('/');
35+
const curPathName = pathSegments[3];
36+
const fullSubRoute = pathSegments.slice(3, 5).join('/');
37+
if (current.serverInfo.error && !canAccess(current.serverInfo, curPathName) && !canAccess(current.serverInfo, fullSubRoute)) {
3638
navigate(`/apps/${current.slug}/overview`, { replace: true });
3739
return <div />;
3840
// return (

src/dashboard/Dashboard.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ const LazyDatabaseProfile = lazy(() => import('./DatabaseProfiler/DatabaseProfil
8383
const LazyEmailVerification = lazy(() => import('./Notification/EmailVerification.react'));
8484
const LazyEmailPasswordReset = lazy(() => import('./Notification/EmailPasswordReset.react'));
8585
const LazyPushAndroidSettings = lazy(() => import('./Push/PushAndroidSettings.react'));
86+
const LazyPushiOSSettings = lazy(() => import('./Push/PushiOSSettings.react'));
8687

8788

8889
async function fetchHubUser() {
@@ -164,21 +165,22 @@ const preloadMap = {
164165
emailVerification: () => import('./Notification/EmailVerification.react'),
165166
emailPasswordReset: () => import('./Notification/EmailPasswordReset.react'),
166167
pushAndroidSettings: () => import('./Push/PushAndroidSettings.react'),
168+
pushiOSSettings: () => import('./Push/PushiOSSettings.react'),
167169
};
168170

169171
// Preload all routes with proper error handling and logging
170172
const preloadRoute = async (routeName, preloadFn) => {
171173
try {
172174
await preloadFn();
173-
console.log(`Successfully preloaded route: ${routeName}`);
175+
// console.log(`Successfully preloaded route: ${routeName}`);
174176
} catch (err) {
175177
console.error(`Error preloading route ${routeName}:`, err);
176178
}
177179
};
178180

179181
// Preload all routes in parallel
180182
const preloadAllRoutes = () => {
181-
console.log('Preloading routes...');
183+
// console.log('Preloading routes...');
182184
return Promise.all(
183185
Object.entries(preloadMap).map(([routeName, preloadFn]) =>
184186
preloadRoute(routeName, preloadFn)
@@ -217,7 +219,7 @@ class Dashboard extends React.Component {
217219
componentDidMount() {
218220
// Start preloading routes immediately but don't block on it
219221
preloadAllRoutes().finally(() => {
220-
console.log('Route preloading complete');
222+
// console.log('Route preloading complete');
221223
});
222224

223225
get('/parse-dashboard-config.json').then(({ apps, newFeaturesInLatestVersion = [], user }) => {
@@ -507,6 +509,7 @@ class Dashboard extends React.Component {
507509
<Route path="push/audiences" element={<PushAudiencesIndex />} />
508510
<Route path="push/new" element={<PushNew />} />
509511
<Route path="push/android-settings" element={<LazyComponentWrapper><LazyPushAndroidSettings /></LazyComponentWrapper>} />
512+
<Route path="push/ios-settings" element={<LazyComponentWrapper><LazyPushiOSSettings /></LazyComponentWrapper>} />
510513
<Route path="push/:pushId" element={<PushDetails />} />
511514

512515
<Route path="connect" element={<B4aConnectPage />} />

src/dashboard/DashboardView.react.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ export default class DashboardView extends React.Component {
8383
}
8484

8585
render() {
86-
const isLocked = !canAccess(this.context.serverInfo, window.location.pathname.split('/')[3]);
86+
const pathSegments = window.location.pathname.split('/');
87+
const topRoute = pathSegments[3];
88+
const fullSubRoute = pathSegments.slice(3, 5).join('/');
89+
const isLocked = !canAccess(this.context.serverInfo, topRoute)
90+
&& !canAccess(this.context.serverInfo, fullSubRoute);
8791
if (isLocked) {
8892
return (
8993
<div className={baseStyles.pageCenter} style={{ flexDirection: 'column' }}>
@@ -349,19 +353,19 @@ export default class DashboardView extends React.Component {
349353
],
350354
},
351355
{
352-
name: 'Pushes',
356+
name: 'Push',
353357
link: '/push/new',
354358
children: [
355359
{ name: 'Send New Push', link: '/push/new' },
356-
{ name: 'Past Pushes', link: '/push/activity' },
360+
{ name: 'History', link: '/push/activity' },
357361
{ name: 'Audiences', link: '/push/audiences' },
358-
{ name: 'Android', link: '/push/android-settings' },
362+
{ name: 'Setup', link: '/push/android-settings' },
359363
],
360364
},
361365
];
362366

363367
appSidebarSections.push({
364-
name: 'Notification',
368+
name: 'Notifications',
365369
icon: 'b4a-push-notification-icon',
366370
link: '/notification',
367371
subsections: notificationSubSections,

src/dashboard/Data/AppOverview/AppLoadingText.react.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const AppLoadingText = ({ appName, appId, pollSchemas }) => {
3232
document.documentElement.style.setProperty('--text-interval', `${TEXT_INTERVAL}ms`);
3333
document.documentElement.style.setProperty('--fill-duration', `${TEXT_INTERVAL / 2}ms`);
3434
return () => {
35-
console.log('deleting cookie');
35+
// console.log('deleting cookie');
3636
try {
3737
document.cookie = `newApp-${appId}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=back4app.com`;
3838
} catch (error) {

src/dashboard/Notification/EmailPasswordReset.react.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ const renderChangedValuesFooter = (changes, fieldOptions) => {
9191
class EmailPasswordReset extends DashboardView {
9292
constructor() {
9393
super();
94-
this.section = 'Notification';
94+
this.section = 'Notifications';
9595
this.subsection = 'Password Reset';
9696
this.state = {
9797
isLoading: true,
@@ -345,7 +345,7 @@ class EmailPasswordReset extends DashboardView {
345345

346346
renderContent() {
347347
const toolbar = (
348-
<Toolbar section="Notification" subsection="Reset Password Email" />
348+
<Toolbar section="Notifications" subsection="Reset Password Email" />
349349
);
350350
const { isLoading, initialFields, errorMessage, hasPermission, isUserVerified } = this.state;
351351

src/dashboard/Notification/EmailVerification.react.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import B4aModal from 'components/B4aModal/B4aModal.react';
1717
import Button from 'components/Button/Button.react';
1818
import { Link } from 'react-router-dom';
1919
import validateEmailFormat from 'lib/validateEmailFormat';
20+
import { amplitudeLogEvent } from 'lib/amplitudeEvents';
2021

2122
const DEFAULT_VERIFICATION_BODY =
2223
'Hi,\n\n' +
@@ -99,7 +100,7 @@ const renderChangedValuesFooter = (changes, fieldOptions) => {
99100
class EmailVerification extends DashboardView {
100101
constructor() {
101102
super();
102-
this.section = 'Notification';
103+
this.section = 'Notifications';
103104
this.subsection = 'Verification';
104105
this.state = {
105106
isLoading: true,
@@ -483,7 +484,7 @@ class EmailVerification extends DashboardView {
483484

484485
renderContent() {
485486
const toolbar = (
486-
<Toolbar section="Notification" subsection="Email Verification" />
487+
<Toolbar section="Notifications" subsection="Email Verification" />
487488
);
488489
const { isLoading, initialFields, errorMessage, hasPermission, isUserVerified, canChangeEmailTemplate } = this.state;
489490

@@ -553,6 +554,7 @@ class EmailVerification extends DashboardView {
553554
return this.context.updateEmailSettings(emailSettings, preventLoginWithUnverifiedEmail);
554555
}}
555556
afterSave={({ fields, resetFields }) => {
557+
amplitudeLogEvent('Verification email configured');
556558
this.setState({
557559
initialFields: { ...fields },
558560
isDirty: false,
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React, { useState, useCallback } from 'react';
2+
import B4aModal from 'components/B4aModal/B4aModal.react';
3+
import Field from 'components/Field/Field.react';
4+
import Label from 'components/Label/Label.react';
5+
import FileInput from 'components/FileInput/FileInput.react';
6+
import Dropdown from 'components/Dropdown/Dropdown.react';
7+
import Option from 'components/Dropdown/Option.react';
8+
import RadioButton from 'components/RadioButton/RadioButton.react';
9+
import FormNote from 'components/FormNote/FormNote.react';
10+
import styles from './PushiOSSettings.scss';
11+
12+
function validate(fields) {
13+
const errors = {};
14+
15+
if (!fields.file) {
16+
errors.file = 'APNs certificate file is required.';
17+
} else if (!fields.file.name.endsWith('.p12')) {
18+
errors.file = 'File must have a .p12 extension.';
19+
}
20+
21+
if (!fields.deviceType) {
22+
errors.deviceType = 'Device type is required.';
23+
}
24+
25+
return errors;
26+
}
27+
28+
const P12CertificateModal = ({ deviceTypes, onSave, onClose }) => {
29+
const [file, setFile] = useState(null);
30+
const [deviceType, setDeviceType] = useState('ios');
31+
const [production, setProduction] = useState(true);
32+
const [errors, setErrors] = useState({});
33+
const [saving, setSaving] = useState(false);
34+
const [serverError, setServerError] = useState(null);
35+
36+
const handleSubmit = useCallback(async () => {
37+
const fields = { file, deviceType };
38+
const validationErrors = validate(fields);
39+
setErrors(validationErrors);
40+
41+
if (Object.keys(validationErrors).length > 0) {
42+
return;
43+
}
44+
45+
setSaving(true);
46+
setServerError(null);
47+
try {
48+
await onSave(file, deviceType, production);
49+
setSaving(false);
50+
onClose();
51+
} catch (err) {
52+
const msg = typeof err === 'string' ? err
53+
: (err && err.error) || (err && err.message) || 'Failed to save certificate.';
54+
setServerError(msg);
55+
setSaving(false);
56+
}
57+
}, [file, deviceType, production, onSave]);
58+
59+
const firstError = Object.values(errors).find(Boolean);
60+
61+
return (
62+
<B4aModal
63+
type={B4aModal.Types.DEFAULT}
64+
title="APNs Certificate"
65+
width={700}
66+
confirmText={saving ? 'Saving\u2026' : 'Save'}
67+
cancelText="Cancel"
68+
onConfirm={handleSubmit}
69+
onCancel={onClose}
70+
disabled={saving}
71+
progress={saving}
72+
canCancel={!saving}
73+
>
74+
<Field
75+
label={<Label text="APNs Certificate File" description="Upload your .p12 certificate file" />}
76+
input={
77+
<div className={styles.fileInputField}>
78+
<FileInput
79+
onChange={f => { setFile(f); setErrors(prev => ({ ...prev, file: undefined })); setServerError(null); }}
80+
accept=".p12,application/x-pkcs12"
81+
value={file ? { name: file.name } : undefined}
82+
/>
83+
</div>
84+
}
85+
/>
86+
<Field
87+
label={<Label text="Device Type" />}
88+
input={
89+
<Dropdown
90+
value={deviceType}
91+
onChange={value => { setDeviceType(value); setErrors(prev => ({ ...prev, deviceType: undefined })); }}
92+
placeHolder="Select device type"
93+
dark={false}
94+
fixed={true}
95+
>
96+
{deviceTypes.map(type => (
97+
<Option key={type} value={type}>{type}</Option>
98+
))}
99+
</Dropdown>
100+
}
101+
/>
102+
<Field
103+
label={<Label text="Certificate type" description="Development or production" />}
104+
input={
105+
<div className={styles.radiobuttonWrapper}>
106+
<label htmlFor="p12_development" className={styles.radioOption}>
107+
<RadioButton
108+
dark={true}
109+
id="p12_development"
110+
name="p12Production"
111+
checked={!production}
112+
onChange={() => setProduction(false)}
113+
/>Development
114+
</label>
115+
<label htmlFor="p12_production" className={styles.radioOption}>
116+
<RadioButton
117+
dark={true}
118+
id="p12_production"
119+
name="p12Production"
120+
checked={production}
121+
onChange={() => setProduction(true)}
122+
/>Production
123+
</label>
124+
</div>
125+
}
126+
/>
127+
<FormNote show={!!firstError} color="red">
128+
{firstError}
129+
</FormNote>
130+
<FormNote show={!!serverError && !firstError} color="red">
131+
{serverError}
132+
</FormNote>
133+
</B4aModal>
134+
);
135+
};
136+
137+
export default P12CertificateModal;

0 commit comments

Comments
 (0)