Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions apps/frontend/src/components/launches/add.provider.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,68 @@ const ChromeExtensionWarning: FC<{
);
};

export const FacebookPageGuidance: FC<{
identifier: string;
onConfirm: () => void;
}> = ({ identifier, onConfirm }) => {
const modals = useModals();
const t = useT();
return (
<div className="flex flex-col gap-[16px] pt-[8px]">
<p className="text-[14px] text-textColor/80">
{t(
'facebook_guidance_intro',
'You will be redirected to Facebook to authorize Postiz.'
)}
</p>
<ul className="flex flex-col gap-[8px] list-disc ps-[20px] text-[14px] text-textColor/80">
<li>
{identifier === 'instagram'
? t(
'facebook_guidance_instagram_page',
'Select the Facebook page your Instagram business account is connected to — granting a different page will not work.'
)
: t(
'facebook_guidance_facebook_page',
'Select the Facebook page(s) you want to post to.'
)}
</li>
<li>
{t(
'facebook_guidance_shared_grant',
'Your page selection applies to all your Facebook and Instagram channels in Postiz, not only the one you are connecting — keep the pages used by your other channels checked, or those channels will stop working.'
)}
</li>
<li>
{t(
'facebook_guidance_edit_settings',
"If you connected before, Facebook may reuse your previous selection. Click 'Edit settings' on the Facebook screen to change it."
)}
</li>
</ul>
<div className="flex gap-[10px] mt-[8px]">
<Button
type="button"
className="flex-1"
onClick={() => {
modals.closeCurrent();
onConfirm();
}}
>
{t('continue', 'Continue')}
</Button>
<Button
type="button"
className="flex-1 !bg-transparent border border-tableBorder text-textColor"
onClick={() => modals.closeCurrent()}
>
{t('cancel', 'Cancel')}
</Button>
</div>
</div>
);
};

export const AddProviderComponent: FC<{
social: Array<{
identifier: string;
Expand Down Expand Up @@ -662,6 +724,22 @@ export const AddProviderComponent: FC<{
});
return;
}
if (
!invite &&
(identifier === 'instagram' || identifier === 'facebook')
) {
modal.openModal({
title: t('before_you_continue', 'Before you continue'),
withCloseButton: true,
children: (
<FacebookPageGuidance
identifier={identifier}
onConfirm={() => gotoIntegration()}
/>
),
});
return;
}
await gotoIntegration();
},
[onboarding]
Expand Down
10 changes: 10 additions & 0 deletions apps/frontend/src/components/launches/continue.integration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,16 @@ export const ContinueIntegration: FC<{
existingId={[]}
initialData={twoStepState.pages}
isSaving={isSaving}
onBack={
logged || twoStepState.returnURL
? () =>
navigateOrShow(
'/launches',
twoStepState.returnURL,
'Channel setup cancelled'
)
: undefined
}
/>
</IntegrationContext.Provider>
</div>
Expand Down
75 changes: 65 additions & 10 deletions apps/frontend/src/components/launches/launches.component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
'use client';

import { AddProviderButton } from '@gitroom/frontend/components/launches/add.provider.component';
import {
AddProviderButton,
FacebookPageGuidance,
} from '@gitroom/frontend/components/launches/add.provider.component';
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import SafeImage from '@gitroom/react/helpers/safe.image';
import { capitalize, groupBy, orderBy } from 'lodash';
Expand Down Expand Up @@ -358,6 +362,7 @@ export const LaunchesComponent = () => {
const toast = useToaster();
const fireEvents = useFireEvents();
const t = useT();
const modal = useModals();
const [reload, setReload] = useState(false);
const [collapseMenu, setCollapseMenu] = useCookie('collapseMenu', '0');
const [mode] = useCookie('mode', 'dark');
Expand Down Expand Up @@ -431,6 +436,33 @@ export const LaunchesComponent = () => {
}, []);
const continueIntegration = useCallback(
(integration: any) => async () => {
// For Facebook/Instagram, finishing a half-connected channel in-app
// only works when the Facebook grant is correct — and when it isn't,
// the picker is a dead end. Start over through the OAuth flow
// instead, same as "Add Channel" (the connect upserts the same
// channel, so nothing is lost).
if (
integration.identifier === 'instagram' ||
integration.identifier === 'facebook'
) {
modal.openModal({
title: t('before_you_continue', 'Before you continue'),
withCloseButton: true,
children: (
<FacebookPageGuidance
identifier={integration.identifier}
onConfirm={async () => {
const { url } = await (
await fetch(`/integrations/social/${integration.identifier}`)
).json();
window.location.href = url;
}}
/>
),
});
return;
}

router.push(
`/launches?added=${integration.identifier}&continue=${integration.id}`
);
Expand All @@ -444,15 +476,38 @@ export const LaunchesComponent = () => {
}
) =>
async () => {
const { url } = await (
await fetch(
`/integrations/social/${integration.identifier}?refresh=${integration.internalId}`,
{
method: 'GET',
}
)
).json();
window.location.href = url;
const gotoRefresh = async () => {
const { url } = await (
await fetch(
`/integrations/social/${integration.identifier}?refresh=${integration.internalId}`,
{
method: 'GET',
}
)
).json();
window.location.href = url;
};

// Same guidance as the "Add Channel" path — reconnecting goes
// through the identical Facebook screens.
if (
integration.identifier === 'instagram' ||
integration.identifier === 'facebook'
) {
modal.openModal({
title: t('before_you_continue', 'Before you continue'),
withCloseButton: true,
children: (
<FacebookPageGuidance
identifier={integration.identifier}
onConfirm={() => gotoRefresh()}
/>
),
});
return;
}

await gotoRefresh();
},
[]
);
Expand Down
29 changes: 25 additions & 4 deletions apps/frontend/src/components/layout/continue.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { continueProviderList } from '@gitroom/frontend/components/new-launch/providers/continue-provider/list';
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const Null: FC<{
onSave: (data: any) => Promise<void>;
existingId: string[];
Expand Down Expand Up @@ -71,13 +73,32 @@ const ModalContent: FC<{
integrations: string[];
}> = ({ continueId, added, provider: Provider, closeModal, integrations }) => {
const fetch = useFetch();
const toaster = useToaster();
const t = useT();

const onSave = useCallback(
async (data: any) => {
await fetch(`/integrations/provider/${continueId}/connect`, {
method: 'POST',
body: JSON.stringify(data),
});
const response = await fetch(
`/integrations/provider/${continueId}/connect`,
{
method: 'POST',
body: JSON.stringify(data),
}
);
if (!response.ok) {
const { message } = await response
.json()
.catch(() => ({ message: '' }));
toaster.show(
message ||
t(
'could_not_connect_channel',
'Could not connect the channel, please try again'
),
'warning'
);
return;
}
closeModal();
},
[continueId, closeModal]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export interface ContinueProviderProps {
existingId: string[];
initialData?: any[];
isSaving?: boolean;
// When provided, the empty state shows a "Back to Postiz" button — used
// by the full-page OAuth landing flow, which has no dialog to close.
onBack?: () => void;
}

export interface EmptyStateMessage {
Expand Down Expand Up @@ -59,7 +62,7 @@ export function withContinueProvider<TItem, TSelection>(
} = config;

return function ContinueProviderComponent(props: ContinueProviderProps) {
const { onSave, existingId, initialData, isSaving } = props;
const { onSave, existingId, initialData, isSaving, onBack } = props;
const call = useCustomProviderFunction();
const t = useT();
const [selection, setSelection] = useState<TSelection | null>(null);
Expand Down Expand Up @@ -119,6 +122,13 @@ export function withContinueProvider<TItem, TSelection>(
)}
</span>
))}
{!!onBack && (
<div className="mt-[24px]">
<Button type="button" onClick={onBack}>
{t('back_to_postiz', 'Back to Postiz')}
</Button>
</div>
)}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export class IntegrationRepository {
...params,
disabled: false,
deletedAt: null,
refreshNeeded: false,
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export class IntegrationService {
await this._notificationService.inAppNotification(
orgId,
`Could not refresh your ${integration.providerIdentifier} channel ${err}`,
`Could not refresh your ${integration.providerIdentifier} channel ${err}. Please go back to the system and connect it again ${process.env.FRONTEND_URL}/launches`,
`Could not refresh your ${integration.providerIdentifier} channel ${err}. No posts can be published to this channel until you reconnect it: go to ${process.env.FRONTEND_URL}/launches, click 'Add Channel' and select the same account — this replaces the broken connection and keeps all your scheduled posts. There is no need to delete the channel.`,
true,
false,
'info'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
};
}

if (/"code":\s*190\b/.test(body)) {
return {
type: 'refresh-token' as const,
value:
'The Facebook access token is invalid, please reconnect the channel',
};
}

if (body.indexOf('1366046') > -1) {
return {
type: 'bad-body' as const,
Expand Down Expand Up @@ -243,6 +251,13 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
`${process.env.FRONTEND_URL}/integrations/social/facebook`
)}` +
`&state=${state}` +
// Re-prompt permissions/assets the user previously declined —
// without this Facebook silently returns the same reduced grant on
// every reconnect, so a bad grant can never be repaired from our UI.
// Note: it does NOT skip the "Reconnect / Edit settings" screen when
// the existing grant is intact; changing the page selection still
// requires the user to click "Edit settings" there.
`&auth_type=rerequest` +
`&scope=${this.scopes.join(',')}`,
codeVerifier: makeId(10),
state,
Expand Down Expand Up @@ -383,7 +398,10 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
// Business Manager API not available for all users
}

return allPages;
// Pages without an access_token were never granted to the app in the
// OAuth dialog (e.g. only discovered through Business Manager).
// Publishing through them is impossible, so don't offer them.
return allPages.filter((p: any) => p.access_token);
}

async fetchPageInformation(accessToken: string, data: { page: string }) {
Expand Down
Loading
Loading