Skip to content
Merged
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
94 changes: 54 additions & 40 deletions src/components/CippWizard/CippAddTenantTypeSelection.jsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,82 @@
import { Avatar, Card, CardContent, Stack, SvgIcon, Typography } from "@mui/material";
import { useState, useEffect } from "react";
import { CippWizardStepButtons } from "./CippWizardStepButtons";
import { BuildingOfficeIcon, CloudIcon } from "@heroicons/react/24/outline";
import { Avatar, Card, CardContent, Stack, SvgIcon, Typography } from '@mui/material'
import { useState, useEffect } from 'react'
import { CippWizardStepButtons } from './CippWizardStepButtons'
import { BuildingOfficeIcon, CloudIcon, LinkIcon } from '@heroicons/react/24/outline'

export const CippAddTenantTypeSelection = (props) => {
const { onNextStep, formControl, currentStep, onPreviousStep } = props;
const { onNextStep, formControl, currentStep, onPreviousStep } = props

const [selectedOption, setSelectedOption] = useState(null);
const [selectedOption, setSelectedOption] = useState(null)

// Register the tenantType field in react-hook-form
formControl.register("tenantType", {
formControl.register('tenantType', {
required: true,
});
})

// Restore selection if already set (when navigating back)
useEffect(() => {
const currentValue = formControl.getValues("tenantType");
const currentValue = formControl.getValues('tenantType')
if (currentValue) {
setSelectedOption(currentValue);
setSelectedOption(currentValue)
}
// Restore the form's selectedOption state if navigating back
const selectedOptionValue = formControl.getValues("selectedOption");
const selectedOptionValue = formControl.getValues('selectedOption')
if (selectedOptionValue) {
formControl.setValue("selectedOption", selectedOptionValue);
formControl.setValue('selectedOption', selectedOptionValue)
}
}, [formControl]);
}, [formControl])

const handleOptionClick = (value) => {
setSelectedOption(value);
formControl.setValue("tenantType", value);
setSelectedOption(value)
formControl.setValue('tenantType', value)

// Clear validation fields from other paths when changing selection
// This ensures going back and choosing a different option doesn't keep old validations
if (value === "GDAP") {
if (value === 'GDAP') {
// Clear Direct tenant fields
formControl.unregister("DirectTenantAuth");
} else if (value === "Direct") {
formControl.unregister('DirectTenantAuth')
} else if (value === 'Direct') {
// Clear GDAP fields
formControl.unregister("GDAPTemplate");
formControl.unregister("GDAPInviteAccepted");
formControl.unregister("GDAPRelationshipId");
formControl.unregister("GDAPOnboardingComplete");
formControl.unregister('GDAPTemplate')
formControl.unregister('GDAPInviteAccepted')
formControl.unregister('GDAPRelationshipId')
formControl.unregister('GDAPOnboardingComplete')
} else if (value === 'IndirectReseller') {
// Clear other paths
formControl.unregister('DirectTenantAuth')
formControl.unregister('GDAPTemplate')
formControl.unregister('GDAPInviteAccepted')
formControl.unregister('GDAPRelationshipId')
formControl.unregister('GDAPOnboardingComplete')
}

// Trigger validation only for the tenantType field
formControl.trigger("tenantType");
};
formControl.trigger('tenantType')
}

const options = [
{
value: "GDAP",
label: "Add GDAP Tenant",
value: 'GDAP',
label: 'Add GDAP Tenant',
description:
"Select this option to add a new tenant to your Microsoft Partner center environment. We'll walk you through the steps of setting up GDAP.",
icon: <CloudIcon />,
},
{
value: "Direct",
label: "Add Direct Tenant",
value: 'Direct',
label: 'Add Direct Tenant',
description:
"Select this option if you are not a Microsoft partner, or want to add a tenant outside of the scope of your partner center.",
'Select this option if you are not a Microsoft partner, or want to add a tenant outside of the scope of your partner center.',
icon: <BuildingOfficeIcon />,
},
];
{
value: 'IndirectReseller',
label: 'Get Indirect Reseller Invite Link',
description:
'Generate a reseller relationship invite link to send to a customer. This does not add the tenant to CIPP, but may be used by other vendors to populate their customer list.',
icon: <LinkIcon />,
},
]

return (
<Stack spacing={3}>
Expand All @@ -74,19 +88,19 @@ export const CippAddTenantTypeSelection = (props) => {
</Stack>
<Stack spacing={2}>
{options.map((option) => {
const isSelected = selectedOption === option.value;
const isSelected = selectedOption === option.value

return (
<Card
key={option.value}
onClick={() => handleOptionClick(option.value)}
variant="outlined"
sx={{
cursor: "pointer",
cursor: 'pointer',
...(isSelected && {
boxShadow: (theme) => `0px 0px 0px 2px ${theme.palette.primary.main}`,
}),
"&:hover": {
'&:hover': {
...(isSelected ? {} : { boxShadow: 8 }),
},
}}
Expand All @@ -96,9 +110,9 @@ export const CippAddTenantTypeSelection = (props) => {
<Avatar
variant="rounded"
sx={{
backgroundColor: "background.default",
borderColor: "divider",
borderStyle: "solid",
backgroundColor: 'background.default',
borderColor: 'divider',
borderStyle: 'solid',
borderWidth: 1,
}}
>
Expand All @@ -111,7 +125,7 @@ export const CippAddTenantTypeSelection = (props) => {
</Stack>
</CardContent>
</Card>
);
)
})}
</Stack>
<CippWizardStepButtons
Expand All @@ -121,7 +135,7 @@ export const CippAddTenantTypeSelection = (props) => {
formControl={formControl}
/>
</Stack>
);
};
)
}

export default CippAddTenantTypeSelection;
export default CippAddTenantTypeSelection
141 changes: 141 additions & 0 deletions src/components/CippWizard/CippIndirectResellerLink.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { useEffect, useMemo, useState } from 'react'
import { Alert, Autocomplete, Box, Skeleton, Stack, TextField, Typography } from '@mui/material'
import { ApiGetCall } from '../../api/ApiCall'
import { CippWizardStepButtons } from './CippWizardStepButtons'
import { CippCopyToClipBoard } from '../CippComponents/CippCopyToClipboard'

export const CippIndirectResellerLink = (props) => {
const { formControl, currentStep, onPreviousStep, onNextStep } = props
const [selectedProvider, setSelectedProvider] = useState(null)

const linkData = ApiGetCall({
url: '/api/ListResellerRelationshipLink',
queryKey: 'ListResellerRelationshipLink',
})

const inviteUrl = linkData.data?.inviteUrl ?? null
const indirectProviders = linkData.data?.indirectProviders ?? []
const inviteUrlError = linkData.data?.inviteUrlError ?? null

const finalUrl = useMemo(() => {
if (!inviteUrl) return null
if (!selectedProvider) return inviteUrl
// Append the indirect provider ID before the # fragment
const hashIndex = inviteUrl.indexOf('#')
const base = hashIndex !== -1 ? inviteUrl.slice(0, hashIndex) : inviteUrl
const hash = hashIndex !== -1 ? inviteUrl.slice(hashIndex) : ''
return `${base}&indirectCSPId=${selectedProvider.id}${hash}`
}, [inviteUrl, selectedProvider])

const providerOptions = useMemo(
() =>
indirectProviders.map((p) => ({
label: p.name,
id: p.id,
mpnId: p.mpnId,
location: p.location,
})),
[indirectProviders]
)

return (
<Stack spacing={3}>
<Box>
<Typography variant="h6" gutterBottom>
Indirect Reseller Relationship Link
</Typography>
<Typography variant="body2" color="text.secondary">
Generate an invite link to send to a customer so they can authorize you as their indirect
reseller. This does <strong>not</strong> add the tenant to CIPP — it only provides the
Microsoft Admin Portal invitation link.
</Typography>
</Box>

{linkData.isFetching && (
<Stack spacing={2}>
{/* Indirect provider dropdown skeleton */}
<Skeleton variant="rounded" height={56} />
{/* Link field skeleton */}
<Stack spacing={0.5}>
<Skeleton variant="text" width={80} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="text" width="60%" />
</Stack>
</Stack>
)}

{linkData.isError && (
<Alert severity="error">
Failed to load relationship link from the Partner Center API. Ensure your CIPP application
has the required Partner Center permissions.
</Alert>
)}

{inviteUrlError && !linkData.isError && <Alert severity="warning">{inviteUrlError}</Alert>}

{!linkData.isFetching && !linkData.isError && inviteUrl && (
<>
{indirectProviders.length > 0 && (
<Autocomplete
options={providerOptions}
value={selectedProvider}
onChange={(_, value) => setSelectedProvider(value)}
getOptionLabel={(option) => option.label}
renderOption={(renderProps, option) => (
<li {...renderProps} key={option.id}>
<Stack>
<Typography variant="body2">{option.label}</Typography>
<Typography variant="caption" color="text.secondary">
MPN ID: {option.mpnId} · {option.location}
</Typography>
</Stack>
</li>
)}
renderInput={(params) => (
<TextField
{...params}
label="Indirect Provider (optional)"
placeholder="Select to include a direct reseller in the invite"
helperText="If you resell through an indirect provider (e.g. PAX8), select them here to include their ID in the link."
/>
)}
/>
)}

<Box>
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>Invite Link</strong>
</Typography>
<Stack direction="row" spacing={1} alignItems="center">
<TextField
fullWidth
value={finalUrl}
inputProps={{ readOnly: true }}
size="small"
sx={{ fontFamily: 'monospace' }}
/>
<CippCopyToClipBoard text={finalUrl} />
</Stack>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
Send this link to your customer. When they follow it, they will be linked to your
reseller account in the Microsoft Admin Portal.
</Typography>
</Box>

<Alert severity="info">
There is no automatic confirmation when the customer accepts this invite. You can verify
the relationship in Partner Center once the customer has completed the process.
</Alert>
</>
)}

<CippWizardStepButtons
currentStep={currentStep}
onPreviousStep={onPreviousStep}
onNextStep={onNextStep}
formControl={formControl}
noSubmitButton
/>
</Stack>
)
}
7 changes: 7 additions & 0 deletions src/components/CippWizard/OnboardingWizardPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CippAlertsStep } from './CippAlertsStep.jsx'
import { CippAddTenantTypeSelection } from './CippAddTenantTypeSelection.jsx'
import { CippDirectTenantDeploy } from './CippDirectTenantDeploy.jsx'
import { CippGDAPTenantSetup } from './CippGDAPTenantSetup.jsx'
import { CippIndirectResellerLink } from './CippIndirectResellerLink.jsx'
import { CippGDAPTenantOnboarding } from './CippGDAPTenantOnboarding.jsx'
import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from '@heroicons/react/24/outline'
import { useRouter } from 'next/router'
Expand Down Expand Up @@ -103,6 +104,12 @@ const OnboardingWizardPage = () => {
showStepWhen: (values) =>
values?.selectedOption === 'AddTenant' && values?.tenantType === 'GDAP',
},
{
description: 'Reseller Link',
component: CippIndirectResellerLink,
showStepWhen: (values) =>
values?.selectedOption === 'AddTenant' && values?.tenantType === 'IndirectReseller',
},
{
description: 'GDAP Onboarding',
component: CippGDAPTenantOnboarding,
Expand Down