Skip to content
82 changes: 57 additions & 25 deletions src/components/Vehicles/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,69 @@
import React from 'react';
import PrimaryButton from '../Shared/PrimaryButton';

interface FooterProps {
canShare: boolean;
onCancel: () => void;
onShare: () => void;
selectedVehiclesCount: number;
onUpdatePermissions: () => void;
hasVehicleWithOldPermissions: boolean;
}

const Footer = ({
canShare,
onCancel,
onShare,
selectedVehiclesCount,
}: {
canShare: boolean;
onCancel: () => void;
onShare: () => void;
selectedVehiclesCount: number;
}) => {
onUpdatePermissions,
hasVehicleWithOldPermissions,
}: FooterProps) => {
const showContinueButton = !canShare;
const showUpdatePermissionsButton = hasVehicleWithOldPermissions && canShare;
const showShareButtons = !hasVehicleWithOldPermissions && canShare;

const renderContinueButton = () => {
return (
<div className="flex flex-row gap-2 justify-center">
<PrimaryButton onClick={onCancel}>Continue</PrimaryButton>
</div>
);
};

const renderCancelAndShareButtons = () => {
return (
<div className="grid grid-cols-2 gap-4 justify-between">
<button
onClick={onCancel}
className="bg-white font-medium text-[#09090B] border border-gray-300 px-4 py-2 rounded-3xl hover:border-gray-500"
>
Cancel
</button>
<PrimaryButton onClick={onShare} disabled={selectedVehiclesCount === 0}>
Save changes
</PrimaryButton>
</div>
);
};

const renderUpdatePermissionsButton = () => {
return (
<div className="flex flex-col gap-2 items-center">
<p className="text-xs text-gray-500">
There are vehicles with old permissions, update them before continuing
</p>
<PrimaryButton onClick={onUpdatePermissions} className="w-full">
Update vehicles permissions
</PrimaryButton>
</div>
);
};

return (
<div
className={`grid grid-flow-col auto-cols-fr gap-4 ${
canShare ? 'justify-between' : 'justify-center'
} w-full max-w-[440px] pt-4`}
>
{!canShare && <PrimaryButton onClick={onCancel}>Continue</PrimaryButton>}
{canShare && (
<>
<button
onClick={onCancel}
className="bg-white font-medium text-[#09090B] border border-gray-300 px-4 py-2 rounded-3xl hover:border-gray-500"
>
Cancel
</button>
<PrimaryButton onClick={onShare} disabled={selectedVehiclesCount === 0}>
Save changes
</PrimaryButton>
</>
)}
<div className={`flex flex-col gap-4 w-full max-w-[440px] pt-4`}>
{showContinueButton && renderContinueButton()}
{showUpdatePermissionsButton && renderUpdatePermissionsButton()}
{showShareButtons && renderCancelAndShareButtons()}
</div>
);
};
Expand Down
23 changes: 2 additions & 21 deletions src/components/Vehicles/ManageVehicleDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,9 @@ import React from 'react';
import { Vehicle } from '../../models/vehicle';
import Header from '../Shared/Header';
import { SharedPermissionsNote } from './';
import { useDevCredentials } from '../../context/DevCredentialsContext';
import { VehicleManagerMandatoryParams } from '../../types';
import { hasUpdatedPermissions } from '../../utils/permissions';

export const ManageVehicleDetails = ({ vehicle }: { vehicle: Vehicle }) => {
const { permissions, permissionTemplateId } =
useDevCredentials<VehicleManagerMandatoryParams>();

const {
tokenId,
shared,
expiresAt,
make,
model,
year,
permissions: vehiclePermissions,
} = vehicle;
const hasUpdatedPerms = hasUpdatedPermissions(
vehiclePermissions,
permissions,
permissionTemplateId,
);
const { tokenId, shared, expiresAt, make, model, year, hasOldPermissions } = vehicle;

return (
<>
Expand All @@ -41,7 +22,7 @@ export const ManageVehicleDetails = ({ vehicle }: { vehicle: Vehicle }) => {

<p className="text-center mt-8">Shared until {expiresAt}</p>

<SharedPermissionsNote shared={shared} hasUpdatedPermissions={hasUpdatedPerms} />
<SharedPermissionsNote shared={shared} hasOldPermissions={hasOldPermissions} />
</>
);
};
20 changes: 20 additions & 0 deletions src/components/Vehicles/SelectVehicles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const SelectVehicles: React.FC = () => {
incompatibleVehicles,
hasNextPage,
hasPreviousPage,
hasVehicleWithOldPermissions,
} = useFetchVehicles();
const {
selectedVehicles,
Expand Down Expand Up @@ -68,6 +69,23 @@ export const SelectVehicles: React.FC = () => {
}
};

const handleUpdatePermissions = async () => {
try {
setLoadingState(true, 'Updating vehicles permissions', true);
const vehiclesWithOldPermissions = vehicles.filter(
({ hasOldPermissions }) => hasOldPermissions,
);
await handleShareVehicles(vehiclesWithOldPermissions);
} catch (err) {
captureException(err);
if (!isInvalidSessionError(err)) {
setError('Failed to update vehicles permissions');
}
} finally {
setLoadingState(false);
}
};

const onCancel = () => {
finishShareVehicles([]);
};
Expand Down Expand Up @@ -121,6 +139,8 @@ export const SelectVehicles: React.FC = () => {
onCancel={onCancel}
onShare={handleShare}
selectedVehiclesCount={selectedVehicles.length}
onUpdatePermissions={handleUpdatePermissions}
hasVehicleWithOldPermissions={hasVehicleWithOldPermissions}
/>
</>
</UIManagerLoaderWrapper>
Expand Down
10 changes: 5 additions & 5 deletions src/components/Vehicles/SharedPermissionsNote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import React from 'react';

interface SharedPermissionsNoteProps {
shared: boolean;
hasUpdatedPermissions: boolean;
hasOldPermissions: boolean;
}

export const SharedPermissionsNote: React.FC<SharedPermissionsNoteProps> = ({
shared,
hasUpdatedPermissions,
hasOldPermissions,
}) => {
if (!shared || hasUpdatedPermissions) {
if (!shared || !hasOldPermissions) {
return null;
}

return (
<p className="text-xs text-gray-500 mt-2">
<span className="font-semibold">Note:</span> Shared with old permissions, revoke and
re-share to ensure service continuity
<span className="font-semibold">Note:</span> Shared with old permissions, update
them to ensure service continuity
</p>
);
};
22 changes: 2 additions & 20 deletions src/components/Vehicles/VehicleCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import { Vehicle } from '../../models/vehicle';
import { UiStates } from '../../enums';
import { useUIManager } from '../../context/UIManagerContext';
import { Checkbox } from '../Shared/Checkbox';
import { useDevCredentials } from '../../context/DevCredentialsContext';
import { VehicleManagerMandatoryParams } from '../../types';
import { SharedPermissionsNote } from './';
import { hasUpdatedPermissions } from '../../utils/permissions';

interface VehicleCardProps {
vehicle: Vehicle;
Expand All @@ -25,23 +22,8 @@ export const VehicleCard: React.FC<VehicleCardProps> = ({
incompatible,
}) => {
const { componentData, setComponentData, setUiState } = useUIManager();
const { permissions, permissionTemplateId } =
useDevCredentials<VehicleManagerMandatoryParams>();

const {
tokenId,
shared,
model,
make,
year,
expiresAt,
permissions: vehiclePermissions,
} = vehicle;
const hasUpdatedPerms = hasUpdatedPermissions(
vehiclePermissions,
permissions,
permissionTemplateId,
);
const { tokenId, shared, model, make, year, expiresAt, hasOldPermissions } = vehicle;

const handleManageClick = (e: React.MouseEvent) => {
setComponentData({ ...componentData, vehicle }); //Retains permissionTemplateID for Manage Vehicle
Expand Down Expand Up @@ -103,7 +85,7 @@ export const VehicleCard: React.FC<VehicleCardProps> = ({
<div className="text-sm text-gray-500 font-medium">ID: {tokenId.toString()}</div>
{shared && <div className="text-sm text-gray-500">Shared Until: {expiresAt}</div>}

<SharedPermissionsNote shared={shared} hasUpdatedPermissions={hasUpdatedPerms} />
<SharedPermissionsNote shared={shared} hasOldPermissions={hasOldPermissions} />
</label>

{/* Manage Vehicle */}
Expand Down
15 changes: 13 additions & 2 deletions src/hooks/useFetchVehicles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ import { VehicleManagerMandatoryParams } from '../types';

export const useFetchVehicles = () => {
const { user } = useAuthContext();
const { clientId, vehicleTokenIds, vehicleMakes, powertrainTypes } =
useDevCredentials<VehicleManagerMandatoryParams>();
const {
clientId,
vehicleTokenIds,
vehicleMakes,
powertrainTypes,
permissionTemplateId,
permissions,
} = useDevCredentials<VehicleManagerMandatoryParams>();
const [startCursor, setStartCursor] = useState('');
const [endCursor, setEndCursor] = useState('');
const [hasNextPage, setHasNextPage] = useState(false);
const [hasPreviousPage, setHasPreviousPage] = useState(false);
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
const [incompatibleVehicles, setIncompatibleVehicles] = useState<Vehicle[]>([]);
const [hasVehicleWithOldPermissions, setHasVehicleWithOldPermissions] = useState(false);

const fetchVehicles = async (direction = 'next') => {
const cursor = direction === 'next' ? endCursor : startCursor;
Expand All @@ -28,9 +35,12 @@ export const useFetchVehicles = () => {
vehicleMakes,
powertrainTypes,
},
permissionTemplateId,
permissions,
});
setVehicles(transformedVehicles.compatibleVehicles);
setIncompatibleVehicles(transformedVehicles.incompatibleVehicles);
setHasVehicleWithOldPermissions(transformedVehicles.hasVehicleWithOldPermissions);
setEndCursor(transformedVehicles.endCursor);
setStartCursor(transformedVehicles.startCursor);
setHasPreviousPage(transformedVehicles.hasPreviousPage);
Expand All @@ -43,6 +53,7 @@ export const useFetchVehicles = () => {
hasPreviousPage,
vehicles,
incompatibleVehicles,
hasVehicleWithOldPermissions,
};
};

Expand Down
2 changes: 2 additions & 0 deletions src/models/__tests__/vehicle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jest.mock('@dimo-network/transactions', () => ({
describe('LocalVehicle', () => {
const mockVehicleNode = {
tokenId: 123,
tokenDID: 'did:erc721:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:123',
imageURI: 'http://image.url',
definition: {
id: 'mock_def_id',
Expand Down Expand Up @@ -49,6 +50,7 @@ describe('LocalVehicle', () => {
it('normalizes vehicle data', () => {
expect(localVehicle.normalize()).toEqual({
tokenId: 123,
tokenDID: 'did:erc721:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:123',
imageURI: 'http://image.url',
make: 'Tesla',
model: 'Model S',
Expand Down
4 changes: 4 additions & 0 deletions src/models/vehicle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import { fetchDeviceDefinition, VehicleNode } from '../services';

export interface Vehicle {
tokenId: number;
tokenDID: string;
imageURI: string;
make: string;
model: string;
year: number;
shared: boolean;
expiresAt: string;
permissions: string;
hasOldPermissions: boolean;
}

export interface VehicleResponse {
compatibleVehicles: Vehicle[];
incompatibleVehicles: Vehicle[];
hasVehicleWithOldPermissions: boolean;
hasNextPage: boolean;
endCursor: string;
hasPreviousPage: boolean;
Expand Down Expand Up @@ -67,6 +70,7 @@ export class LocalVehicle {
normalize() {
return {
tokenId: this.tokenId,
tokenDID: this.vehicleNode.tokenDID,
imageURI: this.vehicleNode.imageURI,
make: this.make,
model: this.model,
Expand Down
4 changes: 4 additions & 0 deletions src/services/__tests__/vehicleService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ beforeEach(() => {
nodes: [
{
tokenId: 1,
tokenDID: 'did:erc721:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:1',
imageURI: 'http://image.url',
definition: {
id: 'tesla_model_3_2019',
Expand All @@ -34,6 +35,7 @@ beforeEach(() => {
},
{
tokenId: 2,
tokenDID: 'did:erc721:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:2',
imageURI: 'http://image.url',
definition: {
id: 'ford_bronco_2023',
Expand Down Expand Up @@ -177,6 +179,7 @@ it('Returns vehicles with the correct shape in compatible and incompatible array
expect(vehicle).toEqual(
expect.objectContaining({
tokenId: expect.any(Number),
tokenDID: expect.any(String),
imageURI: expect.anything(),
shared: expect.any(Boolean),
expiresAt: expect.any(String),
Expand All @@ -201,6 +204,7 @@ it('Returns incompatible vehicles with the correct shape when no compatible vehi
expect(vehicle).toEqual(
expect.objectContaining({
tokenId: expect.any(Number),
tokenDID: expect.any(String),
imageURI: expect.anything(),
shared: expect.any(Boolean),
expiresAt: expect.any(String),
Expand Down
2 changes: 2 additions & 0 deletions src/services/identityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const GET_VEHICLES = gql`
) {
nodes {
tokenId
tokenDID
imageURI
definition {
id
Expand Down Expand Up @@ -62,6 +63,7 @@ const GET_VEHICLES = gql`

export type VehicleNode = {
tokenId: number;
tokenDID: string;
imageURI: string;
definition: {
id: string;
Expand Down
Loading