Skip to content

Commit

Permalink
Merge pull request #9 from DIMO-Network/moiz/sign-permission
Browse files Browse the repository at this point in the history
Sign/Share Permissions Functionality
  • Loading branch information
MoizAhmedd authored Oct 30, 2024
2 parents 398d895 + 18c8def commit 8b9008f
Show file tree
Hide file tree
Showing 12 changed files with 445 additions and 31 deletions.
13 changes: 7 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import "./App.css";
import { useAuthContext } from "./context/AuthContext";
import { useDevCredentials } from "./context/DevCredentialsContext"; // Import DevCredentialsContext
import OtpInput from "./components/Auth/OtpInput";
import VehicleManager from "./components/Vehicles/VehicleManager";

function App() {
const { loading: authLoading } = useAuthContext(); // Get loading state from AuthContext
const { loading: authLoading, authStep } = useAuthContext(); // Get loading state from AuthContext
const { credentialsLoading, clientId, apiKey, redirectUri } = useDevCredentials(); // Get loading state and credentials from DevCredentialsContext
const [authStep, setAuthStep] = useState(0); // 0 = Email Input, 1 = Loading, 2 = Success
const [email, setEmail] = useState("");
const [otpId, setOtpId] = useState(""); // New state for OTP ID

Expand Down Expand Up @@ -43,12 +43,13 @@ function App() {
<img src={logo} alt="Dimo Logo" className="mx-auto mb-6 w-32 h-auto" />

{/* Render components based on authStep */}
{authStep === 0 && <EmailInput onSubmit={setEmail} setAuthStep={setAuthStep} setOtpId={setOtpId}/>}
{authStep === 1 && <OtpInput setAuthStep={setAuthStep} email={email} otpId={otpId}/>}
{authStep === 2 && <SuccessPage />}
{authStep === 0 && <EmailInput onSubmit={setEmail} setOtpId={setOtpId}/>}
{authStep === 1 && <OtpInput email={email} otpId={otpId}/>}
{authStep === 2 && <VehicleManager />}
{authStep === 3 && <SuccessPage />}
</div>
</div>
);
}

export default App;
export default App;
4 changes: 1 addition & 3 deletions src/components/Auth/EmailInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ import { useAuthContext } from "../../context/AuthContext"; // Use the auth cont

interface EmailInputProps {
onSubmit: (email: string) => void;
setAuthStep: (step: number) => void;
setOtpId: (otpId: string) => void;
}

const EmailInput: React.FC<EmailInputProps> = ({
onSubmit,
setAuthStep,
setOtpId,
}) => {
const { sendOtp } = useAuthContext(); // Get sendOtp from the context
const { sendOtp, setAuthStep } = useAuthContext(); // Get sendOtp from the context
const [email, setEmail] = useState("");

const handleSubmit = async () => {
Expand Down
9 changes: 3 additions & 6 deletions src/components/Auth/OtpInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import React, { useState } from "react";
import { useAuthContext } from "../../context/AuthContext"; // Use the auth context

interface OtpInputProps {
setAuthStep: (step: number) => void;
email: string;
otpId: string;
}

const OtpInput: React.FC<OtpInputProps> = ({ setAuthStep, email, otpId }) => {
const { verifyOtp, authenticateUser } = useAuthContext(); // Get verifyOtp from the context
const OtpInput: React.FC<OtpInputProps> = ({ email, otpId }) => {
const { verifyOtp, authenticateUser, setJwt, setAuthStep } = useAuthContext(); // Get verifyOtp from the context
const [otp, setOtp] = useState("");

const handleSubmit = async () => {
Expand All @@ -17,9 +16,7 @@ const OtpInput: React.FC<OtpInputProps> = ({ setAuthStep, email, otpId }) => {
const result = await verifyOtp(email, otp, otpId);

if ( result.success && result.credentialBundle ) {
authenticateUser(email, result.credentialBundle, () => {
setAuthStep(2); // Move to success page after authentication
});
authenticateUser(email, result.credentialBundle, setJwt, setAuthStep);
}
}
};
Expand Down
42 changes: 42 additions & 0 deletions src/components/Vehicles/VehicleCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { Vehicle } from '../../models/vehicle';

interface VehicleCardProps {
vehicle: Vehicle;
isSelected: boolean;
onSelect: () => void;
disabled: boolean;
}

const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, isSelected, onSelect, disabled }) => (
<div
className={`flex items-center p-4 border rounded-lg cursor-pointer ${
disabled ? 'bg-gray-100 text-gray-500 cursor-not-allowed' : 'hover:bg-gray-50'
}`}
onClick={!disabled ? onSelect : undefined}
>
<input
type="checkbox"
checked={isSelected}
onChange={onSelect}
id={`vehicle-${vehicle.tokenId.toString()}`}
disabled={disabled}
className="mr-4"
/>
<label htmlFor={`vehicle-${vehicle.tokenId.toString()}`} className="flex-grow text-left">
<span className="font-semibold">
{vehicle.make} {vehicle.model} ({vehicle.year})
</span>
<span className="text-sm ml-2">
- Vehicle ID: {vehicle.tokenId.toString()}
</span>
{vehicle.shared && (
<span className="ml-2 px-2 py-1 text-xs font-medium text-gray-600 bg-gray-200 rounded">
Shared
</span>
)}
</label>
</div>
);

export default VehicleCard;
171 changes: 171 additions & 0 deletions src/components/Vehicles/VehicleManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { useEffect, useState } from "react";
import { fetchVehiclesWithTransformation } from "../../services/identityService";
import VehicleCard from "./VehicleCard";
import { useAuthContext } from "../../context/AuthContext";
import { Vehicle } from "../../models/vehicle";
import { setVehiclePermissions } from "../../services/turnkeyService";
import { sacdPermissionValue } from "@dimo-network/transactions";
import { sendTokenToParent } from "../../utils/authUtils";
import { useDevCredentials } from "../../context/DevCredentialsContext";
import {
fetchPermissionsFromId,
PermissionTemplate,
} from "../../services/permissionsService";
import { SACD_PERMISSIONS } from "@dimo-network/transactions/dist/core/types/args";

const VehicleManager: React.FC = () => {
const targetGrantee = "0xeAa35540a94e3ebdf80448Ae7c9dE5F42CaB3481"; // TODO: Replace with client ID
const { user, jwt, setAuthStep } = useAuthContext();
const { redirectUri, permissionTemplateId } = useDevCredentials();
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
const [permissionTemplate, setPermissionTemplate] =
useState<PermissionTemplate | null>(null);
const [selectedVehicles, setSelectedVehicles] = useState<Vehicle[]>([]); // Array for multiple selected vehicles

useEffect(() => {
const fetchVehicles = async () => {
if (user?.smartContractAddress && targetGrantee) {
try {
const transformedVehicles = await fetchVehiclesWithTransformation(
user.smartContractAddress,
targetGrantee
);
setVehicles(transformedVehicles);
} catch (error) {
console.error("Error fetching vehicles:", error);
}
}
};

const fetchPermissions = async () => {
if (permissionTemplateId) {
// Ensure permissionTemplateId is defined
try {
const permissionTemplate = await fetchPermissionsFromId(
permissionTemplateId
);
setPermissionTemplate(permissionTemplate);
} catch (error) {
console.error("Error fetching permissions:", error);
}
}
};

// Run both fetches in parallel if they can be independent
Promise.all([fetchVehicles(), fetchPermissions()]);
}, [user?.smartContractAddress, targetGrantee, permissionTemplateId]); // Added permissionTemplateId as a dependency

const handleVehicleSelect = (vehicle: Vehicle) => {
setSelectedVehicles(
(prevSelected) =>
prevSelected.includes(vehicle)
? prevSelected.filter((v) => v !== vehicle) // Deselect if already selected
: [...prevSelected, vehicle] // Add to selected if not already selected
);
};

const handleShare = async () => {
const permissionsObject: SACD_PERMISSIONS = permissionTemplate?.data.scope
.permissions
? permissionTemplate.data.scope.permissions.reduce(
(obj: SACD_PERMISSIONS, permission: string) => {
obj[permission as keyof SACD_PERMISSIONS] = true;
return obj;
},
{} as SACD_PERMISSIONS
)
: {};

console.log(permissionsObject);

const perms = sacdPermissionValue(permissionsObject);

if (selectedVehicles.length > 0 && targetGrantee) {
try {
for (const vehicle of selectedVehicles) {
if (!vehicle.shared) {
await setVehiclePermissions(
vehicle.tokenId,
targetGrantee,
perms,
BigInt(1793310371),
"ipfs://QmRAuxeMnsjPsbwW8LkKtk6Nh6MoqTvyKwP3zwuwJnB2yP"
);
}
}

sendTokenToParent(jwt!, redirectUri!, () => {
//TODO: Better handling of null
setSelectedVehicles([]); // Clear selection after sharing
setAuthStep(3); // Move to success page
});
} catch (error) {
console.error("Error sharing vehicles:", error);
}
} else {
alert("Please select at least one non-shared vehicle to share.");
}
};

const renderDescription = (description: string) => {
return (
<div>
{description.split("\n\n").map((paragraph, index) => (
<React.Fragment key={index}>
{/* Check if the paragraph contains bullet points */}
{paragraph.includes("- ") ? (
<ul className="list-disc list-inside mb-4">
{paragraph.split("\n-").map((line, i) =>
i === 0 ? (
<p key={i} className="font-semibold mb-2">
{line.trim()}
</p>
) : (
<li key={i} className="ml-4">
{line.trim()}
</li>
)
)}
</ul>
) : (
<p className="mb-4">{paragraph}</p>
)}
</React.Fragment>
))}
</div>
);
};

return (
<div className="max-w-md mx-auto p-6 bg-white shadow-lg rounded-lg text-center">
<h1 className="text-2xl font-semibold mb-2">
Select Vehicles to Share with [App Name]
</h1>
<p style={{height:"40vh"}} className="text-sm text-gray-600 mb-6 overflow-scroll">
{permissionTemplate?.data.description
? renderDescription(permissionTemplate?.data.description)
: "The developer is requesting access to view your vehicle data. Select the vehicles you’d like to share access to."}
</p>
<div className="space-y-4">
{vehicles.map((vehicle) => (
<VehicleCard
key={vehicle.tokenId.toString()}
vehicle={vehicle}
isSelected={selectedVehicles.includes(vehicle)}
onSelect={() => handleVehicleSelect(vehicle)}
disabled={vehicle.shared}
/>
))}
</div>
<button
onClick={handleShare}
disabled={selectedVehicles.length === 0}
className="mt-6 px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400 disabled:bg-gray-400"
>
Share Selected
</button>
</div>
);
};

export default VehicleManager;
28 changes: 19 additions & 9 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@ interface AuthContextProps {
authenticateUser: (
email: string,
credentialBundle: string,
onSuccess: (token: string) => void
setJwt: (jwt: string) => void,
setAuthStep: (step: number) => void
) => void;
user: UserObject | null;
loading: boolean; // Add loading state to context
jwt: string | null;
setJwt: React.Dispatch<React.SetStateAction<string | null>>;
authStep: number;
setAuthStep: React.Dispatch<React.SetStateAction<number>>;
}

const AuthContext = createContext<AuthContextProps | undefined>(undefined);
Expand All @@ -58,8 +63,10 @@ export const AuthProvider = ({
}): JSX.Element => {
const [loading, setLoading] = useState(false);
const [user, setUser] = useState<UserObject | null>(null);
const [jwt, setJwt] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const { clientId, apiKey, redirectUri } = useDevCredentials();
const [authStep, setAuthStep] = useState<number>(0); // 0 = Email Input, 1 = Loading, 2 = Success
const { clientId, apiKey, redirectUri, permissionTemplateId } = useDevCredentials();


const handleSendOtp = async (
Expand All @@ -68,7 +75,7 @@ export const AuthProvider = ({
setLoading(true);
setError(null);
try {
const result = await sendOtp(email, apiKey!); // Call the updated sendOtp service
const result = await sendOtp(email, apiKey!); // Call the updated sendOtp service, TODO: Better handling of null
if (result.success && result.otpId) {
console.log(`OTP sent to ${email}, OTP ID: ${result.otpId}`);
return { success: true, otpId: result.otpId };
Expand All @@ -82,10 +89,10 @@ export const AuthProvider = ({
const challenge = resp[1];

//Trigger account creation request
const account = await createAccount(email, apiKey!, attestation as object, challenge as string, true);
const account = await createAccount(email, apiKey!, attestation as object, challenge as string, true); //TODO: Better handling of null

//Send OTP Again
const newOtp = await sendOtp(email, apiKey!); // Call the updated sendOtp service
const newOtp = await sendOtp(email, apiKey!); // Call the updated sendOtp service, //TODO: Better handling of null
if ( newOtp.success && newOtp.otpId) {
console.log("YES");
return { success: true, otpId: newOtp.otpId };
Expand Down Expand Up @@ -132,7 +139,8 @@ export const AuthProvider = ({
const handleAuthenticateUser = async (
email: string,
credentialBundle: string,
onSuccess: (token: string) => void
setJwt: (jwt: string) => void,
setAuthStep: (step: number) => void,
) => {
setLoading(true);
setError(null);
Expand All @@ -144,9 +152,7 @@ export const AuthProvider = ({
}
const user = userDetailsResponse.user;
setUser(user); // Store the user object in the context
await authenticateUser(email, clientId!, redirectUri!, user.subOrganizationId, user.walletAddress, user!.smartContractAddress!, (token) => {
onSuccess(token);
});
await authenticateUser(email, clientId!, redirectUri!, user.subOrganizationId, user.walletAddress, user!.smartContractAddress!, setJwt, setAuthStep, permissionTemplateId); //TODO: Better handling of null
} catch (error) {
console.error(error);
} finally {
Expand All @@ -162,6 +168,10 @@ export const AuthProvider = ({
authenticateUser: handleAuthenticateUser,
loading,
user,
jwt,
setJwt,
authStep,
setAuthStep
}}
>
{children}
Expand Down
Loading

0 comments on commit 8b9008f

Please sign in to comment.