-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [WLEO-218] Add Remote Presentation Cross-Device to Example App (#…
…178) Co-authored-by: LazyAfternoons <[email protected]>
- Loading branch information
1 parent
0774386
commit 631c9cc
Showing
8 changed files
with
372 additions
and
3 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import React, { useMemo } from "react"; | ||
import { useNavigation } from "@react-navigation/native"; | ||
import { useAppSelector } from "../store/utils"; | ||
import TestScenario, { | ||
type TestScenarioProp, | ||
} from "../components/TestScenario"; | ||
import { selectPresentationAsyncStatus } from "../store/reducers/presentation"; | ||
|
||
import { useDebugInfo } from "../hooks/useDebugInfo"; | ||
import { IOVisualCostants, VSpacer } from "@pagopa/io-app-design-system"; | ||
import { FlatList } from "react-native"; | ||
|
||
export const PresentationScreen = () => { | ||
const navigation = useNavigation(); | ||
const presentationState = useAppSelector(selectPresentationAsyncStatus); | ||
|
||
useDebugInfo({ | ||
presentationState, | ||
}); | ||
|
||
const scenarios: Array<TestScenarioProp> = useMemo( | ||
() => [ | ||
{ | ||
title: "PID Remote Cross-Device", | ||
onPress: () => navigation.navigate("QrScanner"), | ||
isLoading: presentationState.isLoading, | ||
hasError: presentationState.hasError, | ||
isDone: presentationState.isDone, | ||
icon: "device", | ||
}, | ||
], | ||
[ | ||
navigation, | ||
presentationState.hasError, | ||
presentationState.isDone, | ||
presentationState.isLoading, | ||
] | ||
); | ||
|
||
return ( | ||
<FlatList | ||
contentContainerStyle={{ | ||
margin: IOVisualCostants.appMarginDefault, | ||
}} | ||
data={scenarios} | ||
keyExtractor={(item, index) => `${item.title}-${index}`} | ||
renderItem={({ item }) => ( | ||
<> | ||
<TestScenario | ||
onPress={item.onPress} | ||
title={item.title} | ||
isLoading={item.isLoading} | ||
hasError={item.hasError} | ||
isDone={item.isDone} | ||
icon={item.icon} | ||
isPresent={item.isPresent} | ||
/> | ||
<VSpacer /> | ||
</> | ||
)} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import React, { useEffect, useState } from "react"; | ||
import { Text, View, Alert, StyleSheet } from "react-native"; | ||
import { | ||
Camera, | ||
useCameraDevice, | ||
useCodeScanner, | ||
} from "react-native-vision-camera"; | ||
import { useAppDispatch } from "../store/utils"; | ||
import { useNavigation } from "@react-navigation/native"; | ||
// Thunk or action you want to dispatch | ||
import { remoteCrossDevicePresentationThunk } from "../thunks/presentation"; | ||
|
||
export const QrScannerScreen = () => { | ||
const dispatch = useAppDispatch(); | ||
const navigation = useNavigation(); | ||
const [hasPermission, setHasPermission] = useState(false); | ||
|
||
const device = useCameraDevice("back"); | ||
|
||
// 3. Ask for camera permission on mount | ||
useEffect(() => { | ||
(async () => { | ||
const cameraPermission = await Camera.requestCameraPermission(); | ||
if (cameraPermission.toString() === "granted") { | ||
setHasPermission(true); | ||
} else { | ||
Alert.alert("Error", "Camera permission not granted!"); | ||
} | ||
})(); | ||
}, []); | ||
|
||
const codeScanner = useCodeScanner({ | ||
codeTypes: ["qr", "ean-13"], | ||
onCodeScanned: (codes) => { | ||
dispatch( | ||
remoteCrossDevicePresentationThunk({ qrcode: codes[0]?.value || "" }) | ||
); | ||
|
||
navigation.goBack(); | ||
}, | ||
}); | ||
|
||
if (!device) { | ||
return ( | ||
<View> | ||
<Text>Camera not available!</Text> | ||
</View> | ||
); | ||
} | ||
|
||
return ( | ||
<View> | ||
{hasPermission ? ( | ||
<Camera | ||
style={style.camera} | ||
device={device} | ||
isActive={true} // optionally disable camera after scanning | ||
codeScanner={codeScanner} | ||
audio={false} | ||
/> | ||
) : ( | ||
<Text>Camera permission not granted!</Text> | ||
)} | ||
</View> | ||
); | ||
}; | ||
|
||
const style = StyleSheet.create({ | ||
camera: { | ||
width: 500, | ||
height: 500, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { createSlice } from "@reduxjs/toolkit"; | ||
|
||
import { remoteCrossDevicePresentationThunk as remoteCrossDevicePresentationThunk } from "../../thunks/presentation"; | ||
import { persistReducer, type PersistConfig } from "redux-persist"; | ||
import AsyncStorage from "@react-native-async-storage/async-storage"; | ||
import { asyncStatusInitial } from "../utils"; | ||
import type { RootState, AsyncStatus } from "../types"; | ||
import { sessionReset } from "./sesssion"; | ||
|
||
/** | ||
* State type definition for the presentation slice | ||
* the state contains: | ||
* - async state: isLoading, isDone, hasError as defined in {@link AsyncStatus} | ||
*/ | ||
type PresentationState = { | ||
redirectUri: string | undefined; | ||
asyncStatus: AsyncStatus; | ||
}; | ||
|
||
// Initial state for the presentation slice | ||
const initialState: PresentationState = { | ||
redirectUri: undefined, | ||
asyncStatus: asyncStatusInitial, | ||
}; | ||
|
||
/** | ||
* Redux slice for the presentation state which contains the key tag used to register the wallet presentation. | ||
*/ | ||
const presentationSlice = createSlice({ | ||
name: "presentation", | ||
initialState, | ||
reducers: { | ||
presentationReset: () => initialState, | ||
}, | ||
extraReducers: (builder) => { | ||
// Dispatched when is created. Sets the key tag in the state and its state to isDone while resetting isLoading and hasError. | ||
builder.addCase( | ||
remoteCrossDevicePresentationThunk.fulfilled, | ||
(state, action) => { | ||
state.redirectUri = action.payload.result.redirect_uri; | ||
state.asyncStatus.isDone = true; | ||
state.asyncStatus.isLoading = initialState.asyncStatus.isLoading; | ||
state.asyncStatus.hasError = initialState.asyncStatus.hasError; | ||
} | ||
); | ||
|
||
// Dispatched when is pending. Sets the state to isLoading and resets isDone and hasError. | ||
builder.addCase(remoteCrossDevicePresentationThunk.pending, (state) => { | ||
state.asyncStatus.isDone = false; | ||
state.asyncStatus.isLoading = true; | ||
state.asyncStatus.hasError = initialState.asyncStatus.hasError; | ||
}); | ||
|
||
// Dispatched when is rejected. Sets the state to hasError and resets isLoading and isDone. | ||
builder.addCase( | ||
remoteCrossDevicePresentationThunk.rejected, | ||
(state, action) => { | ||
state.asyncStatus.isDone = initialState.asyncStatus.isDone; | ||
state.asyncStatus.isLoading = initialState.asyncStatus.isLoading; | ||
state.asyncStatus.hasError = { status: true, error: action.error }; | ||
} | ||
); | ||
|
||
// Reset the attestation state when the session is reset. | ||
builder.addCase(sessionReset, () => initialState); | ||
}, | ||
}); | ||
|
||
/** | ||
* Exports the actions for the presentation slice. | ||
*/ | ||
export const { presentationReset } = presentationSlice.actions; | ||
|
||
/** | ||
* Configuration for the presentation slice to be persisted in the Redux store. | ||
* Only the keyTag is persisted to avoid regenerating the wallet presentation at each app launch. | ||
*/ | ||
const persistConfig: PersistConfig<PresentationState> = { | ||
key: "presentation", | ||
storage: AsyncStorage, | ||
}; | ||
|
||
/** | ||
* Persisted reducer for the presentation slice. | ||
*/ | ||
export const presentationReducer = persistReducer( | ||
persistConfig, | ||
presentationSlice.reducer | ||
); | ||
|
||
/** | ||
* Selects the presentation state from the root state. | ||
* @param state - The root state of the Redux store | ||
* @returns The presentation state as {@link AsyncStatus} | ||
*/ | ||
export const selectPresentationAsyncStatus = (state: RootState) => | ||
state.presentation.asyncStatus; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.