Building Native application with ionic framework

I have created a project with multiple canisters. created the frontend canister with the ionic framework. How to create the native applications.

I tried the commands:
npx cap add android
ionic build
npx cap sync
npx cap copy android
npx cap open android

but the app doesnt work properly am i missing something?

i inspected using the chrome dev tools and got the error:

Uncaught (in promise) Error: Failed to decode CBOR: Error: Failed to parse, input:

I tried updating the agent-js still same issue.

Ah, Ionic! I used it in many projects in the past, and I even organized a local meetup about it in Zürich.

Uncaught (in promise) Error: Failed to decode CBOR: Error: Failed to parse, input:

When you run your app locally in the browser, does it work?

When you test the production build (npm run build) in the browser (as if it were a PWA), does it work?

Side note: You’re using Internet Identity—does it work in an Ionic app? I’m curious how that works, as I assumed WebView doesn’t support postMessage.

1 Like

The app works fine on the web. But after debugging the app in android studio, the app wont let me to login.

Dev and prod build or just dev?

Yeah I’m not sure the login would work but, can you maybe share a sample repo to reproduce the issue if you’ve got one?

I managed to implement the canister calls and it is working fine, but the login cant be implemented. When i click the login button it opens a browser, but the ii says you were sent here for authentication but no request found.

So, you managed to resolve the original Failed to decode CBOR error you shared, right? How did you solve it? It would be helpful if you could share some details in case someone else encounters it in the future.

I might be wrong, but as I mentioned in my previous message, I doubt that postMessage is supported by WebView on mobile devices, so it might not work out of the box and could be the reason for the error you’re encountering. However, these are just assumptions.

In other words, I doubt that Internet Identity can be used with Ionic/Capacitor straight out of the box using only Agent-js as you would on the web. Again, just an assumption.

The CBOR error still occurs but i can make the cansiter calls.

1 Like

Trying it. Also checking any other solutions and examples on the internet.

1 Like

The CBOR is resolved after updating the agent-js to latest version.

1 Like

Cool, good to know! Thanks for the share.

@peterparker i think browser plugin of capacitor works for ii integration with native apps. Can you help me with this approach?

I can’t really take it on myself (so much to do, so little time), but if you share a few technical details, I can pass them on to the team.

export default function Login() {
    const { dispatch } = useContext(GlobalContext);
    var actor1 = socio_backend;
    const [actor, setActor] = useState(null);
    const [II_URL, setII_URL] = useState("");
    const [inLogin, setInLogin] = useState(false);
    const [present, dismiss] = useIonLoading();
    const [presentToast] = useIonToast();

    useEffect(() => {
        if (process.env.DFX_NETWORK === "local") {
            setII_URL(`http://${process.env.CANISTER_ID_INTERNET_IDENTITY}.localhost:4943`);
        } else if (process.env.DFX_NETWORK === "ic") {
            setII_URL("https://identity.ic0.app");
        } else {
            setII_URL(`https://${process.env.CANISTER_ID_INTERNET_IDENTITY}.dfinity.network`);
        }
    }, []);

    useEffect(() => {
        if (Capacitor.isNativePlatform() && inLogin) {
            console.log('Setting up deep link listener in Login.jsx');
            CapApp.addListener('appUrlOpen', handleDeepLink);
        }

        return () => {
            if (Capacitor.isNativePlatform()) {
                console.log('Removing deep link listener in Login.jsx');
                CapApp.removeAllListeners();
            }
        };
    }, [inLogin]);

    async function handleDeepLink(data) {
        console.log('Received deep link data:', data);
        if (data.url.startsWith('io.ionic.starter://auth')) {
            const url = new URL(data.url);
            const authResult = url.searchParams.get('delegation');
            
            if (authResult) {
                try {
                    const authClient = await AuthClient.create();
                    await authClient.setDelegation(authResult);
                    await login(authClient);
                } catch (error) {
                    console.error('Error processing auth result:', error);
                    presentToast({
                        message: "Error processing authentication",
                        duration: 3000,
                        color: "danger",
                    });
                }
            }
        }
    }

    async function login(authClient) {
        setInLogin(true);
        await present({ message: "Logging in..." });
        const identity = authClient.getIdentity();
        const agent = new HttpAgent({ identity });

        if (process.env.DFX_NETWORK !== "ic") {
            await agent.fetchRootKey();
        }

        actor1 = createActor(process.env.CANISTER_ID_SOCIO_BACKEND, {
            agent,
        });

        setActor(actor1);
        const currentUser = await actor1.getUser();
        dispatch({ type: 'SET_ACTOR', payload: actor1 });
        if (currentUser.length === 0) {
            dispatch({ type: 'SET_USER', payload: currentUser });
        } else {
            dispatch({ type: 'SET_USER', payload: currentUser[0] });
        }

        dispatch({ type: 'SET_LOGGED_IN', payload: true });
        setInLogin(false);
        dismiss();
    }

    async function handleLogin(e) {
        e.preventDefault();
        let authClient = await AuthClient.create();
        if (await authClient.isAuthenticated()) {
            await login(authClient);
        } else {
            try {
                setInLogin(true);
                if (Capacitor.isNativePlatform()) {
                    const redirectUrl = 'io.ionic.starter://auth';
                    const identityProviderUrl = `${II_URL}#authorize`;
                    const url = `${identityProviderUrl}?redirect_uri=${encodeURIComponent(redirectUrl)}`;
                    console.log('Opening browser with URL:', url);
                    await Browser.open({ url });
                } else {
                    await new Promise((resolve, reject) => {
                        authClient.login({
                            identityProvider: II_URL,
                            onSuccess: resolve,
                            onError: reject
                        });
                    });
                    await login(authClient);
                }
            } catch (error) {
                console.error('An error occurred during login:', error);
                presentToast({
                    message: "An error occurred during login",
                    duration: 3000,
                    color: "danger",
                });
                setInLogin(false);
            }
        }
    }

    return (
        <button className="btn login-btn" id="Login" disabled={inLogin} onClick={(e) => handleLogin(e)}>
            <IonIcon icon={logInOutline} slot="start" />
            Login
        </button>
    )
}```
1 Like

This is code i have tried to hanlde ii authentication in native apps with the in app browser with cordova plugin. The approach works fine till the authentication, but stays on the identity.ic0.app and do not redirect to the application.

Thanks for sharing. As I mentioned, I doubt this works that way, but I’ll share the snippet and question with the team.

1 Like

I’m sorry but we have no example using Ionic nor Capacitator.

I can suggest to try to implement the flow with II directly yourself how we do in this demo. This implements the II spec directly without AuthClient.

Or you can take a look at the documentation on how to integrate II on mobile devices.

I hope this helps!

1 Like

I will try to implement the flow of authentication.

1 Like

Let us know how it goes. We might want to expand the documentation with your learnings.

@lmuntaner i tried the implementation without using the authclient from the resources you provided. It works until opening the url and authentication. But i cannot make it to the redirecting to the custom scheme for handling the deep links. Here is my code.

export const authWithNativeII = async ({
  url: url_,
  maxTimeToLive,
  allowPinAuthentication,
  derivationOrigin,
  sessionIdentity,
  autoSelectionPrincipal,
}: {
  url: string;
  maxTimeToLive?: bigint;
  allowPinAuthentication?: boolean;
  derivationOrigin?: string;
  autoSelectionPrincipal?: string;
  sessionIdentity: SignIdentity;
}): Promise<{ identity: DelegationIdentity; authnMethod: string }> => {
  // Step 1: Construct the II URL with the authorize hash
  const iiUrl = new URL(url_);
  iiUrl.hash = '#authorize';
  iiUrl.searchParams.append('redirect_uri', 'io.ionic.starter://callback');

  const requestParams = new URLSearchParams({
    kind: "authorize-client",
    sessionPublicKey: JSON.stringify(new Uint8Array(sessionIdentity.getPublicKey().toDer())),
    maxTimeToLive: maxTimeToLive?.toString() ?? '',
    derivationOrigin: derivationOrigin ?? '',
    allowPinAuthentication: allowPinAuthentication ? 'true' : 'false',
    autoSelectionPrincipal: autoSelectionPrincipal ?? '',
  })

  const authUrl = `${iiUrl.toString()}&${requestParams.toString()}`;

  // Step 2: Open the II URL using Capacitor Browser
  await Browser.open({ url: authUrl });

  // Step 3: Listen for the app scheme callback
  const redirectPromise = new Promise<string>(async (resolve, reject) => {
    const handler = await App.addListener('appUrlOpen', (event) => {
      const callbackUrl = event.url;

      // Check if the URL matches the expected scheme
      if (callbackUrl.startsWith('io.ionic.starter://callback')) {
        App.removeAllListeners(); // Remove all listeners once URL is handled
        resolve(callbackUrl);
      } else {
        reject(new Error('Unexpected URL scheme received.'));
      }
    });

    // Fallback in case the listener is never triggered
    setTimeout(() => {
      handler.remove(); // Clean up the listener after timeout
      reject(new Error('Timeout waiting for callback URL.'));
    }, 60000); // 60 seconds timeout
  });

  // Wait for the redirect URL
  const callbackUrl = await redirectPromise;

  // Step 4: Extract the parameters from the callback URL
  const url = new URL(callbackUrl);
  const delegations = url.searchParams.get('delegations');
  const userPublicKey = url.searchParams.get('userPublicKey');
  const authnMethod = url.searchParams.get('authnMethod');

  if (!delegations || !userPublicKey) {
    throw new Error('Missing required parameters in callback URL.');
  }

  // Step 5: Process the response and create the DelegationIdentity
  const response = {
    kind: 'authorize-client-success',
    delegations: JSON.parse(decodeURIComponent(delegations)),
    userPublicKey: Uint8Array.from(
      atob(decodeURIComponent(userPublicKey)),
      (c) => c.charCodeAt(0)
    ),
    authnMethod,
  };

  const identity = identityFromResponse({
    response: response as AuthResponseSuccess,
    sessionIdentity,
  });

  // Step 6: Close the browser (optional)
  await Browser.close();

  return { identity, authnMethod: authnMethod || 'unknown' };
};

is there something missing in the code to redirected to the custom scheme after succesful login or will it be automatic.