Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(IT Wallet): [SIW-1960] Scan QR code for remote presentation #6670

Merged
merged 18 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d73f30d
feat: scan qr code for remote presentation
RiccardoMolinari95 Jan 31, 2025
b2da4d3
Merge branch 'master' into SIW-1960-scan-qr-code-remote-presentation
RiccardoMolinari95 Jan 31, 2025
fc799a5
chore: add missing locales
RiccardoMolinari95 Jan 31, 2025
624a3d8
chore: updated regex for itw_remote barcode
RiccardoMolinari95 Jan 31, 2025
786f4f3
Merge branch 'master' into SIW-1960-scan-qr-code-remote-presentation
RiccardoMolinari95 Jan 31, 2025
196a756
fix: prettify
RiccardoMolinari95 Jan 31, 2025
f28f1c2
chore: improvement itw remote qr code decoding
RiccardoMolinari95 Feb 3, 2025
7a96824
Merge branch 'master' into SIW-1960-scan-qr-code-remote-presentation
RiccardoMolinari95 Feb 3, 2025
1728d91
chore: fix import
RiccardoMolinari95 Feb 3, 2025
2452726
refactor: change qrCodePayload in remoteRequestPayload
RiccardoMolinari95 Feb 3, 2025
e76db18
Merge branch 'master' into SIW-1960-scan-qr-code-remote-presentation
RiccardoMolinari95 Feb 3, 2025
3ffe18f
Merge branch 'master' into SIW-1960-scan-qr-code-remote-presentation
RiccardoMolinari95 Feb 4, 2025
b2316b4
Merge branch 'master' into SIW-1960-scan-qr-code-remote-presentation
RiccardoMolinari95 Feb 5, 2025
bb3467e
Merge branch 'master' into SIW-1960-scan-qr-code-remote-presentation
RiccardoMolinari95 Feb 5, 2025
f21116f
refactor: rename remote presentation context remoteRequestPayload in …
RiccardoMolinari95 Feb 5, 2025
c30320f
refactor: renamed ItwEidClaimsSelectionScreen in ItwRemoteClaimsDiscl…
RiccardoMolinari95 Feb 6, 2025
d8033ad
Merge branch 'master' into SIW-1960-scan-qr-code-remote-presentation
RiccardoMolinari95 Feb 6, 2025
e01b533
Merge branch 'master' into SIW-1960-scan-qr-code-remote-presentation
RiccardoMolinari95 Feb 6, 2025
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
4 changes: 4 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3533,6 +3533,10 @@ features:
title: Non siamo riusciti a caricare {{credentialName}}
content: Chiudi e riapri l'app per riprovare.
primaryAction: Ho capito
remote:
loadingScreen:
title: Stiamo facendo alcune verifiche di sicurezza...
subtitle: Attendi qualche secondo
trustmark:
description: Mostra il QR Code per attestare l’autenticità del documento quando ti viene richiesto.
qrCode: QR code autenticità credenziale
Expand Down
4 changes: 4 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3533,6 +3533,10 @@ features:
title: Non siamo riusciti a caricare {{credentialName}}
content: Chiudi e riapri l'app per riprovare.
primaryAction: Ho capito
remote:
loadingScreen:
title: Stiamo facendo alcune verifiche di sicurezza...
subtitle: Attendi qualche secondo
trustmark:
description: Mostra il QR Code per attestare l’autenticità del documento quando ti viene richiesto.
qrCode: QR code autenticità credenziale
Expand Down
9 changes: 9 additions & 0 deletions ts/features/barcode/screens/BarcodeScanScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { useHardwareBackButton } from "../../../hooks/useHardwareBackButton";
import { usePagoPaPayment } from "../../payments/checkout/hooks/usePagoPaPayment";
import { FCI_ROUTES } from "../../fci/navigation/routes";
import { paymentAnalyticsDataSelector } from "../../payments/history/store/selectors";
import { ITW_REMOTE_ROUTES } from "../../itwallet/presentation/remote/navigation/routes.ts";

const BarcodeScanScreen = () => {
const navigation = useNavigation<IOStackNavigationProp<AppParamsList>>();
Expand Down Expand Up @@ -155,6 +156,14 @@ const BarcodeScanScreen = () => {
}
});
break;
case "ITW_REMOTE":
navigation.navigate(ITW_REMOTE_ROUTES.MAIN, {
screen: ITW_REMOTE_ROUTES.CLAIMS_DISCLOSURE,
params: {
itwRemoteRequestPayload: barcode.itwRemoteRequestPayload
}
});
break;
}
};

Expand Down
67 changes: 67 additions & 0 deletions ts/features/barcode/types/__tests__/decoders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,71 @@ describe("test decodeIOBarcode function", () => {
expect(output).toStrictEqual(O.none);
});
});

describe("test ITW_REMOTE barcode type", () => {
it("should return O.some on valid QRCode content", () => {
const value =
"https://continua.io.pagopa.it/itw/auth?client_id=abc123xy&request_uri=https%3A%2F%2Fexample.com%2Fcallback&state=hyqizm592";

const output = decodeIOBarcode(value);

expect(output).toStrictEqual(
O.some({
type: "ITW_REMOTE",
itwRemoteRequestPayload: {
clientId: "abc123xy",
requestUri: "https://example.com/callback",
state: "hyqizm592",
requestUriMethod: "GET"
}
})
);
});

it("should decode request_uri_method if provided", () => {
const value =
"https://continua.io.pagopa.it/itw/auth?client_id=abc123xy&request_uri=https%3A%2F%2Fexample.com%2Fcallback&state=hyqizm592&request_uri_method=POST";

const output = decodeIOBarcode(value);

expect(output).toStrictEqual(
O.some({
type: "ITW_REMOTE",
itwRemoteRequestPayload: {
clientId: "abc123xy",
requestUri: "https://example.com/callback",
state: "hyqizm592",
requestUriMethod: "POST"
}
})
);
});

it("should return O.none if request_uri is missing", () => {
const value =
"https://continua.io.pagopa.it/itw/auth?client_id=abc123xy&state=hyqizm592";

const output = decodeIOBarcode(value);

expect(output).toStrictEqual(O.none);
});

it("should return O.none if client_id is missing", () => {
const value =
"https://continua.io.pagopa.it/itw/auth?request_uri=https%3A%2F%2Fexample.com%2Fcallback&state=hyqizm592";

const output = decodeIOBarcode(value);

expect(output).toStrictEqual(O.none);
});

it("should return O.none if both client_id and request_uri are missing", () => {
const value =
"https://continua.io.pagopa.it/itw/auth?state=hyqizm592&request_uri_method=POST";

const output = decodeIOBarcode(value);

expect(output).toStrictEqual(O.none);
});
});
});
37 changes: 36 additions & 1 deletion ts/features/barcode/types/decoders.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not forget to add tests for the new decoder :)

Copy link
Collaborator Author

@RiccardoMolinari95 RiccardoMolinari95 Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reminder! I hadn't seen the test folder before. I've added the tests and made corrections in f28f1c2

Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import * as A from "fp-ts/lib/Array";
import * as E from "fp-ts/lib/Either";
import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import { sequenceS } from "fp-ts/lib/Apply";
import { decodePosteDataMatrix } from "../../../utils/payment";
import { SignatureRequestDetailView } from "../../../../definitions/fci/SignatureRequestDetailView";
import { ItwRemoteRequestPayload } from "../../itwallet/presentation/remote/Utils/itwRemoteTypeUtils.ts";
import { getUrlParam } from "../../itwallet/common/utils/itwUrlUtils.ts";
import { IOBarcodeType } from "./IOBarcode";

// Discriminated barcode type
Expand Down Expand Up @@ -48,6 +51,10 @@ export type DecodedIOBarcode =
| {
type: "FCI";
signatureRequestId: SignatureRequestDetailView["id"];
}
| {
type: "ITW_REMOTE";
itwRemoteRequestPayload: ItwRemoteRequestPayload;
};

// Barcode decoder function which is used to determine the type and content of a barcode
Expand Down Expand Up @@ -107,6 +114,33 @@ const decodeFciBarcode: IOBarcodeDecoderFn = (data: string) =>
}))
);

const decodeItwRemoteBarcode: IOBarcodeDecoderFn = (data: string) =>
pipe(
O.fromNullable(
data.match(/^https:\/\/continua\.io\.pagopa\.it\/itw\/auth\?(.*)$/)
),
O.chain(([url]) =>
sequenceS(O.Monad)({
clientId: getUrlParam(url, "client_id"),
requestUri: getUrlParam(url, "request_uri"),
state: getUrlParam(url, "state"),
requestUriMethod: pipe(
getUrlParam(url, "request_uri_method"),
O.alt(() => O.some("GET"))
)
})
),
O.map(({ clientId, requestUri, state, requestUriMethod }) => ({
type: "ITW_REMOTE",
itwRemoteRequestPayload: {
clientId,
requestUri,
state,
requestUriMethod
}
}))
);

// Each type comes with its own decoded function which is used to identify the barcode content
// To add a new barcode type, add a new entry to this object
//
Expand All @@ -120,7 +154,8 @@ const decodeFciBarcode: IOBarcodeDecoderFn = (data: string) =>
export const IOBarcodeDecoders: IOBarcodeDecodersType = {
IDPAY: decodeIdPayBarcode,
PAGOPA: decodePagoPABarcode,
FCI: decodeFciBarcode
FCI: decodeFciBarcode,
ITW_REMOTE: decodeItwRemoteBarcode
};

type DecodeOptions = {
Expand Down
8 changes: 8 additions & 0 deletions ts/features/itwallet/common/utils/itwUrlUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as O from "fp-ts/Option";
import { pipe } from "fp-ts/function";

export const getUrlParam = (url: string, paramName: string): O.Option<string> =>
pipe(
O.tryCatch(() => new URL(url)),
O.chainNullableK(({ searchParams }) => searchParams.get(paramName))
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// TODO: This will be imported from io-react-native-wallet, when the type will be available
// Remote presentation QR code data
export type ItwRemoteRequestPayload = {
clientId: string;
requestUri: string;
state: string;
requestUriMethod?: string;
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { createActor } from "xstate";
import { itwRemoteMachine } from "../machine.ts";

const T_CLIENT_ID = "clientId";
const T_REQUEST_URI = "https://example.com";
const T_STATE = "state";

describe("itwRemoteMachine", () => {
const mockedMachine = itwRemoteMachine.provide({
actions: {},
Expand All @@ -18,4 +22,25 @@ describe("itwRemoteMachine", () => {

expect(actor.getSnapshot().value).toStrictEqual("Idle");
});

it("should transition from Idle to RemoteRequestValidation when receiving start event", () => {
const actor = createActor(mockedMachine);
actor.start();

actor.send({
type: "start",
payload: {
clientId: T_CLIENT_ID,
requestUri: T_REQUEST_URI,
state: T_STATE
}
});

expect(actor.getSnapshot().value).toStrictEqual("RemoteRequestValidation");
expect(actor.getSnapshot().context.payload).toStrictEqual({
clientId: T_CLIENT_ID,
requestUri: T_REQUEST_URI,
state: T_STATE
});
});
});
20 changes: 17 additions & 3 deletions ts/features/itwallet/presentation/remote/machine/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
// This file is a placeholder for future implementation
export type Context = object;
import { ItwRemoteRequestPayload } from "../Utils/itwRemoteTypeUtils.ts";
import { RemoteFailure } from "./failure.ts";

export const InitialContext: Context = {};
export type Context = {
/**
* The remote request payload for the remote presentation
*/
payload: ItwRemoteRequestPayload | undefined;
/**
* The failure of the remote presentation machine
*/
failure?: RemoteFailure;
};

export const InitialContext: Context = {
payload: undefined,
failure: undefined
};
14 changes: 10 additions & 4 deletions ts/features/itwallet/presentation/remote/machine/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// This file is a placeholder for future implementation
export type PlaceHolder = {
type: "PLACEHOLDER";
import { ItwRemoteRequestPayload } from "../Utils/itwRemoteTypeUtils.ts";

export type Start = {
type: "start";
payload: ItwRemoteRequestPayload;
};

export type Back = {
type: "back";
};

export type RemoteEvents = PlaceHolder;
export type RemoteEvents = Start | Back;
21 changes: 17 additions & 4 deletions ts/features/itwallet/presentation/remote/machine/machine.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { assign, setup } from "xstate";
import { RemoteEvents } from "./events.ts";
import { Context, InitialContext } from "./context.ts";
import { mapEventToFailure } from "./failure.ts";
import { ItwTags } from "../../../machine/tags";
import { InitialContext, Context } from "./context";
import { mapEventToFailure } from "./failure";
import { RemoteEvents } from "./events";

export const itwRemoteMachine = setup({
types: {
Expand All @@ -20,7 +21,19 @@ export const itwRemoteMachine = setup({
states: {
Idle: {
description:
"The machine is in idle, ready to start the remote presentation flow"
"The machine is in idle, ready to start the remote presentation flow",
on: {
start: {
actions: assign(({ event }) => ({
payload: event.payload
})),
target: "RemoteRequestValidation"
}
}
},
RemoteRequestValidation: {
description: "Validating the remote request payload before proceeding",
tags: [ItwTags.Loading]
},
Failure: {
description: "This state is reached when an error occurs"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ItwRemoteClaimsDisclosureScreenNavigationParams } from "../screens/ItwRemoteClaimsDisclosureScreen.tsx";
import { ITW_REMOTE_ROUTES } from "./routes.ts";

export type ItwRemoteParamsList = {
[ITW_REMOTE_ROUTES.CLAIMS_DISCLOSURE]: ItwRemoteClaimsDisclosureScreenNavigationParams;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createStackNavigator } from "@react-navigation/stack";
import { isGestureEnabled } from "../../../../../utils/navigation.ts";
import {
ItwRemoteMachineContext,
ItwRemoteMachineProvider
} from "../machine/provider.tsx";
import { ItwRemoteClaimsDisclosureScreen } from "../screens/ItwRemoteClaimsDisclosureScreen.tsx";
import { ITW_REMOTE_ROUTES } from "./routes.ts";
import { ItwRemoteParamsList } from "./ItwRemoteParamsList.ts";

const Stack = createStackNavigator<ItwRemoteParamsList>();

const hiddenHeader = { headerShown: false };

export const ItwRemoteStackNavigator = () => (
<ItwRemoteMachineProvider>
<InnerNavigator />
</ItwRemoteMachineProvider>
);

const InnerNavigator = () => {
const itwRemoteMachineRef = ItwRemoteMachineContext.useActorRef();

return (
<Stack.Navigator
initialRouteName={ITW_REMOTE_ROUTES.CLAIMS_DISCLOSURE}
screenOptions={{ gestureEnabled: isGestureEnabled }}
screenListeners={{
beforeRemove: () => {
itwRemoteMachineRef.send({ type: "back" });
}
}}
>
<Stack.Screen
name={ITW_REMOTE_ROUTES.CLAIMS_DISCLOSURE}
component={ItwRemoteClaimsDisclosureScreen}
options={hiddenHeader}
/>
</Stack.Navigator>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const ITW_REMOTE_ROUTES = {
MAIN: "ITW_REMOTE_MAIN" as const,
CLAIMS_DISCLOSURE: "ITW_REMOTE_CLAIMS_DISCLOSURE" as const
};
Loading
Loading