Imagine you're a developer of an online wallet mobile app, and the owner of the local cinema approaches you with a request. He wants you to add a feature to the app that allows users to book and pay for seats directly from the app. You happily oblige, and start working on the Android and iOS versions. After a successful launch, another person approaches you and asks if you can add his online store to your application so that customers can pay for goods directly from there. You start to worry about how much bigger the app will get with all these new features. You don't want users to have to download a billion GB app just to pay for their movie tickets.
What will you do? Let's solve this issue together.
We're thinking of ways to make the app lighter and easier to use for all these new features. One idea is to use web-based solutions for payments and bookings directly from the app. To make the integration as simple as possible, we can create a JavaScript SDK.
After some research, we discover the Ionic team's project called Ionic Portals. According to the documentation, it's a supercharged native webview component for iOS and Android. With Ionic Portals, we can:
- Extend native apps with web content. It allows users to add web-based features and experiences to an existing native mobile app.
- Access native features through the Capacitor bridge. Safely and securely access features like the camera, geolocation, haptics, and more - all from the webview. This way we can deliver truly native mobile experiences using the web.
- Control access to native features and data. Native development teams have granular control over which parts of the app - including specific features and data - web teams can access when collaborating on a mobile project. ...
In short, Ionic Portals is the perfect solution for our problem. It allows us to add web-based features to our app without making it heavy. It also provides a secure way to access native features and data, which means we can deliver truly native mobile experiences using the web, making the app lightweight, efficient and user-friendly. With this, we can keep our users happy and enjoy their popcorn while watching the latest blockbuster.
Let's get down to business! To make the integration process as simple as possible, we're going to write a JavaScript SDK. We're going to create a library for interacting web applications with a native application and publish it on npm. After some tweaking, I generated a typescript project using vite and created a message.ts file. The file contains, for the beginning, 4 methods (The necessary imports and missing parts of code can be found on the Github repository and the JS SDK can be easily installed via npm.):
import Portals, { getInitialContext } from "@ionic/portals";
import {
IInitialContext,
IMessage,
IMessageSubscription,
IPortalSubscription,
ISubscribeOptions,
ISubscriptionCallback,
} from "./types";
export const SendMessage = async (params: IMessage) => {
return Portals.publish<IMessage>(params);
};
export const SubscribeToMessage = async (
options: ISubscribeOptions,
callback: ISubscriptionCallback
): Promise<IPortalSubscription> => {
return Portals.subscribe<IMessageSubscription>(options, callback);
};
export const UnsubscribeFromMessage = async (
options: IPortalSubscription
): Promise<void> => {
return Portals.unsubscribe(options);
};
export const GetInitialContext = <T = unknown>():
| IInitialContext<T>
| undefined => {
return getInitialContext();
};
We're creating a frontend project using the famous Create-React-App. And what's the best way to start? By creating an empty react project, of course! Let's create a component:
import { useEffect, useState, useCallback, useRef } from "react";
import {
SendMessage,
SubscribeToMessage,
UnsubscribeFromMessage,
GetInitialContext,
IPortalSubscription,
} from "js-miniapp-bridge";
import { MESSAGE_TOPICS } from "./constants";
function App() {
const tokenSubscriptionRef = useRef<IPortalSubscription>();
const initialContext = GetInitialContext<{ data: string }>();
const [message, setMessage] = useState("");
const [subscribeToTokenSuccessResponse, setSubscribeToTokenSuccessResponse] =
useState<Partial<IPortalSubscription>>({});
const [context, setContext] = useState(initialContext);
const handlePublishToken = async () => {
SendMessage({
topic: MESSAGE_TOPICS.ACCOUNT_TOKEN,
data: message,
});
};
const subscribeToTokenCallback = (result: { topic: string; data: string }) => {
setSubscribeToTokenSuccessResponse(result);
};
const subscribeToToken = useCallback(async () => {
try {
tokenSubscriptionRef.current = await SubscribeToMessage(
{ topic: MESSAGE_TOPICS.CHAT_MESSAGE },
subscribeToTokenCallback
);
} catch (e) {
setsubscribeToTokenErrorResponse(e);
}
}, []);
const handleChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setMessage(e.target.value);
};
useEffect(() => {
subscribeToToken();
return () => {
UnsubscribeFromMessage(tokenSubscriptionRef.current!);
};
}, [subscribeToToken]);
useEffect(() => {
setContext(initialContext);
}, [initialContext]);
return (
<div className="App">
<div>
<b>Subscribe Token Success Result:</b>
<pre>{JSON.stringify(subscribeToTokenSuccessResponse, null, 4)}</pre>
</div>
<hr />
<div>
<b>Initial Context Result:</b>
<pre>{JSON.stringify(context, null, 4)}</pre>
</div>
<hr />
<div>
<b>Message:</b>
</div>
<div>
<input value={message} onChange={handleChangeInput} />
</div>
<button onClick={() => handlePublishToken()}>
Publish/Send Message
</button>
</div>
);
}
export default App;
Now we need to create an application that can subscribe and send messages. To accomplish this, we will generate a React Native project and create the necessary component:
import React, {useState} from 'react';
import {SafeAreaView, ScrollView, View, Text, Button} from 'react-native';
import {
PortalView,
publish,
subscribe,
unsubscribe,
} from '@ionic/portals-react-native';
import initializePortals from './initializePortals';
initializePortals();
function App(): JSX.Element {
const [subscribeTokenSuccessResponse, setSubscribeTokenSuccessResponse] =
useState(null);
const subscribeToTokenRef = React.useRef<number>();
const handlePublishToken = async () => {
publish('CHAT_MESSAGE', 'token from native');
};
const subscribeToTokenCallback = (result: {topic: string; data: any}) => {
setSubscribeTokenSuccessResponse(result);
};
const subscribeToToken = React.useCallback(async () => {
subscribeToTokenRef.current = await subscribe(
'ACCOUNT_TOKEN',
subscribeToTokenCallback,
);
}, []);
React.useEffect(() => {
subscribeToToken();
return () => {
unsubscribe('ACCOUNT_TOKEN', subscribeToTokenRef.current!);
};
}, [subscribeToToken]);
return (
<SafeAreaView>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<View>
<PortalView
portal={{
name: 'hello',
initialContext: {
data: 'Initial data from Native',
},
}}
/>
<Text>Subscribe Token Success Result:</Text>
<Text>
{JSON.stringify(subscribeTokenSuccessResponse || '', null, 4)}
</Text>
<Button title="Publish Token" onPress={handlePublishToken} />
</View>
</ScrollView>
</SafeAreaView>
);
}
export default App;
And that's it. We have created a lightweight, efficient, and user-friendly application that still provides all necessary features. You can see the result in the video demonstration by clicking this link.
By using this method, we will achieve a balance where users can have a lightweight app that still provides all the features they need.
As previously mentioned, all the code can be found on GitHub. If you have any questions or feedback, please leave a comment. I appreciate your input. Have a great day!
Top comments (0)