diff --git a/src/components/Vehicles/Footer.tsx b/src/components/Vehicles/Footer.tsx index 9341e83..a022201 100644 --- a/src/components/Vehicles/Footer.tsx +++ b/src/components/Vehicles/Footer.tsx @@ -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 ( +
+ Continue +
+ ); + }; + + const renderCancelAndShareButtons = () => { + return ( +
+ + + Save changes + +
+ ); + }; + + const renderUpdatePermissionsButton = () => { + return ( +
+

+ There are vehicles with old permissions, update them before continuing +

+ + Update vehicles permissions + +
+ ); + }; + return ( -
- {!canShare && Continue} - {canShare && ( - <> - - - Save changes - - - )} +
+ {showContinueButton && renderContinueButton()} + {showUpdatePermissionsButton && renderUpdatePermissionsButton()} + {showShareButtons && renderCancelAndShareButtons()}
); }; diff --git a/src/components/Vehicles/ManageVehicleDetails.tsx b/src/components/Vehicles/ManageVehicleDetails.tsx index 9fd38df..1273e03 100644 --- a/src/components/Vehicles/ManageVehicleDetails.tsx +++ b/src/components/Vehicles/ManageVehicleDetails.tsx @@ -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(); - - 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 ( <> @@ -41,7 +22,7 @@ export const ManageVehicleDetails = ({ vehicle }: { vehicle: Vehicle }) => {

Shared until {expiresAt}

- + ); }; diff --git a/src/components/Vehicles/SelectVehicles.tsx b/src/components/Vehicles/SelectVehicles.tsx index 6afc11f..6d240ae 100644 --- a/src/components/Vehicles/SelectVehicles.tsx +++ b/src/components/Vehicles/SelectVehicles.tsx @@ -23,6 +23,7 @@ export const SelectVehicles: React.FC = () => { incompatibleVehicles, hasNextPage, hasPreviousPage, + hasVehicleWithOldPermissions, } = useFetchVehicles(); const { selectedVehicles, @@ -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([]); }; @@ -121,6 +139,8 @@ export const SelectVehicles: React.FC = () => { onCancel={onCancel} onShare={handleShare} selectedVehiclesCount={selectedVehicles.length} + onUpdatePermissions={handleUpdatePermissions} + hasVehicleWithOldPermissions={hasVehicleWithOldPermissions} /> diff --git a/src/components/Vehicles/SharedPermissionsNote.tsx b/src/components/Vehicles/SharedPermissionsNote.tsx index 828c754..e123a30 100644 --- a/src/components/Vehicles/SharedPermissionsNote.tsx +++ b/src/components/Vehicles/SharedPermissionsNote.tsx @@ -2,21 +2,21 @@ import React from 'react'; interface SharedPermissionsNoteProps { shared: boolean; - hasUpdatedPermissions: boolean; + hasOldPermissions: boolean; } export const SharedPermissionsNote: React.FC = ({ shared, - hasUpdatedPermissions, + hasOldPermissions, }) => { - if (!shared || hasUpdatedPermissions) { + if (!shared || !hasOldPermissions) { return null; } return (

- Note: Shared with old permissions, revoke and - re-share to ensure service continuity + Note: Shared with old permissions, update + them to ensure service continuity

); }; diff --git a/src/components/Vehicles/VehicleCard.tsx b/src/components/Vehicles/VehicleCard.tsx index f82e82b..46f0783 100644 --- a/src/components/Vehicles/VehicleCard.tsx +++ b/src/components/Vehicles/VehicleCard.tsx @@ -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; @@ -25,23 +22,8 @@ export const VehicleCard: React.FC = ({ incompatible, }) => { const { componentData, setComponentData, setUiState } = useUIManager(); - const { permissions, permissionTemplateId } = - useDevCredentials(); - 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 @@ -103,7 +85,7 @@ export const VehicleCard: React.FC = ({
ID: {tokenId.toString()}
{shared &&
Shared Until: {expiresAt}
} - + {/* Manage Vehicle */} diff --git a/src/hooks/useFetchVehicles.ts b/src/hooks/useFetchVehicles.ts index 0712dd5..94e4633 100644 --- a/src/hooks/useFetchVehicles.ts +++ b/src/hooks/useFetchVehicles.ts @@ -7,14 +7,21 @@ import { VehicleManagerMandatoryParams } from '../types'; export const useFetchVehicles = () => { const { user } = useAuthContext(); - const { clientId, vehicleTokenIds, vehicleMakes, powertrainTypes } = - useDevCredentials(); + const { + clientId, + vehicleTokenIds, + vehicleMakes, + powertrainTypes, + permissionTemplateId, + permissions, + } = useDevCredentials(); const [startCursor, setStartCursor] = useState(''); const [endCursor, setEndCursor] = useState(''); const [hasNextPage, setHasNextPage] = useState(false); const [hasPreviousPage, setHasPreviousPage] = useState(false); const [vehicles, setVehicles] = useState([]); const [incompatibleVehicles, setIncompatibleVehicles] = useState([]); + const [hasVehicleWithOldPermissions, setHasVehicleWithOldPermissions] = useState(false); const fetchVehicles = async (direction = 'next') => { const cursor = direction === 'next' ? endCursor : startCursor; @@ -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); @@ -43,6 +53,7 @@ export const useFetchVehicles = () => { hasPreviousPage, vehicles, incompatibleVehicles, + hasVehicleWithOldPermissions, }; }; diff --git a/src/models/__tests__/vehicle.test.ts b/src/models/__tests__/vehicle.test.ts index 37dd9b2..5d2417e 100644 --- a/src/models/__tests__/vehicle.test.ts +++ b/src/models/__tests__/vehicle.test.ts @@ -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', @@ -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', diff --git a/src/models/vehicle.ts b/src/models/vehicle.ts index 1a11747..9109eb0 100644 --- a/src/models/vehicle.ts +++ b/src/models/vehicle.ts @@ -4,6 +4,7 @@ import { fetchDeviceDefinition, VehicleNode } from '../services'; export interface Vehicle { tokenId: number; + tokenDID: string; imageURI: string; make: string; model: string; @@ -11,11 +12,13 @@ export interface Vehicle { shared: boolean; expiresAt: string; permissions: string; + hasOldPermissions: boolean; } export interface VehicleResponse { compatibleVehicles: Vehicle[]; incompatibleVehicles: Vehicle[]; + hasVehicleWithOldPermissions: boolean; hasNextPage: boolean; endCursor: string; hasPreviousPage: boolean; @@ -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, diff --git a/src/services/__tests__/vehicleService.test.ts b/src/services/__tests__/vehicleService.test.ts index c6a6e01..e015ae8 100644 --- a/src/services/__tests__/vehicleService.test.ts +++ b/src/services/__tests__/vehicleService.test.ts @@ -21,6 +21,7 @@ beforeEach(() => { nodes: [ { tokenId: 1, + tokenDID: 'did:erc721:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:1', imageURI: 'http://image.url', definition: { id: 'tesla_model_3_2019', @@ -34,6 +35,7 @@ beforeEach(() => { }, { tokenId: 2, + tokenDID: 'did:erc721:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:2', imageURI: 'http://image.url', definition: { id: 'ford_bronco_2023', @@ -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), @@ -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), diff --git a/src/services/identityService.ts b/src/services/identityService.ts index 1413124..7a2be67 100644 --- a/src/services/identityService.ts +++ b/src/services/identityService.ts @@ -35,6 +35,7 @@ const GET_VEHICLES = gql` ) { nodes { tokenId + tokenDID imageURI definition { id @@ -62,6 +63,7 @@ const GET_VEHICLES = gql` export type VehicleNode = { tokenId: number; + tokenDID: string; imageURI: string; definition: { id: string; diff --git a/src/services/vehicleService.ts b/src/services/vehicleService.ts index a6e786a..e097cd3 100644 --- a/src/services/vehicleService.ts +++ b/src/services/vehicleService.ts @@ -3,10 +3,23 @@ import { fetchVehicles } from './identityService'; import { IParams } from '../types'; import { sortVehiclesByFilters, transformVehicles } from '../utils/vehicles'; +interface FetchVehiclesWithTransformationParams extends IParams { + permissionTemplateId: string; + permissions: string; +} + export const fetchVehiclesWithTransformation = async ( - params: IParams, + params: FetchVehiclesWithTransformationParams, ): Promise => { - const { ownerAddress, targetGrantee, cursor, direction, filters = {} } = params; + const { + ownerAddress, + targetGrantee, + cursor, + direction, + filters = {}, + permissionTemplateId, + permissions, + } = params; const { data: { @@ -19,12 +32,32 @@ export const fetchVehiclesWithTransformation = async ( filters, ); + const transformedCompatibleVehicles = transformVehicles({ + vehicles: compatibleVehicles, + grantee: targetGrantee, + permissionTemplateId, + permissions, + }); + + const transformedIncompatibleVehicles = transformVehicles({ + vehicles: incompatibleVehicles, + grantee: targetGrantee, + permissionTemplateId, + permissions, + }); + + const hasVehicleWithOldPermissions = [ + ...transformedCompatibleVehicles, + ...transformedIncompatibleVehicles, + ].some((vehicle) => vehicle.hasOldPermissions); + return { hasNextPage: pageInfo.hasNextPage, hasPreviousPage: pageInfo.hasPreviousPage, startCursor: pageInfo.startCursor || '', endCursor: pageInfo.endCursor || '', - compatibleVehicles: transformVehicles(compatibleVehicles, targetGrantee), - incompatibleVehicles: transformVehicles(incompatibleVehicles, targetGrantee), + compatibleVehicles: transformedCompatibleVehicles, + incompatibleVehicles: transformedIncompatibleVehicles, + hasVehicleWithOldPermissions, }; }; diff --git a/src/utils/vehicles.ts b/src/utils/vehicles.ts index ef23f06..bfef311 100644 --- a/src/utils/vehicles.ts +++ b/src/utils/vehicles.ts @@ -1,26 +1,59 @@ import { VehicleFilters, VehiclePermissionsAction } from '../types'; import { LocalVehicle, Vehicle } from '../models/vehicle'; import { extendByYear, formatDate, parseExpirationDate } from './dateUtils'; +import { hasUpdatedPermissions } from './permissions'; -const transformVehicle = ( - vehicle: LocalVehicle, - grantee: `0x${string}` | null, -): Vehicle => { +interface TransformVehicleParams { + vehicle: LocalVehicle; + grantee: `0x${string}` | null; + permissionTemplateId: string; + permissions: string; +} + +const transformVehicle = ({ + vehicle, + grantee, + permissionTemplateId, + permissions, +}: TransformVehicleParams): Vehicle => { const sacd = vehicle.getSacdForGrantee(grantee); + const vehiclePermissions = sacd ? sacd.permissions : '0'; + const updatedPermissions = hasUpdatedPermissions( + vehiclePermissions, + permissions, + permissionTemplateId, + ); return { ...vehicle.normalize(), - permissions: sacd ? sacd.permissions : '0', + permissions: vehiclePermissions, shared: !!sacd, expiresAt: sacd ? formatDate(sacd.expiresAt) : '', + hasOldPermissions: updatedPermissions, }; }; -export const transformVehicles = ( - vehicles: LocalVehicle[], - grantee: `0x${string}` | null, -) => { +interface TransformVehiclesParams { + vehicles: LocalVehicle[]; + grantee: `0x${string}` | null; + permissionTemplateId: string; + permissions: string; +} + +export const transformVehicles = ({ + vehicles, + grantee, + permissionTemplateId, + permissions, +}: TransformVehiclesParams) => { return vehicles - .map((vehicle) => transformVehicle(vehicle, grantee)) + .map((vehicle) => + transformVehicle({ + vehicle, + grantee, + permissionTemplateId, + permissions, + }), + ) .sort((a: any, b: any) => Number(b.shared) - Number(a.shared)); };