LearnInkWebView component for React Native. Both call your own backend to fetch a sign-in token — your backend is responsible for calling the LearnInk Identify API securely.
Before using these, replace the following constants at the top of the file:
YOUR_BACKEND_AUTH_URL— your backend endpoint that calls the LearnInk Identify API and returns a tokenORG_ID— your LearnInk organisation ID
- JavaScript
- TypeScript
import React, { useState, useRef, useEffect } from "react"
import { View, ActivityIndicator, StyleSheet, Alert, Modal } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"
import { WebView } from "react-native-webview"
// Configuration constants
const YOUR_BACKEND_AUTH_URL = "https://your-api.com/auth/learnink"
const WEBVIEW_BASE_URL = "https://m.learn.ink"
const ORG_ID = "acme"
const REQUEST_TIMEOUT = 5000
const MESSAGE_TYPES = {
CLOSE: "CLOSE",
SESSION_EXPIRED: "SESSION_EXPIRED",
}
const LearnInkWebView = ({ path, onClose, userId }) => {
const [webViewUrl, setWebViewUrl] = useState(undefined)
const [loading, setLoading] = useState(true)
const webViewRef = useRef(null)
useEffect(() => {
authenticateUser()
}, [userId])
const buildWebViewUrl = (token) => {
const params = new URLSearchParams({
webview: "true",
...(token && { token }),
})
return `${WEBVIEW_BASE_URL}/${ORG_ID}/${path}?${params.toString()}`
}
const authenticateUser = async () => {
setLoading(true)
try {
const token = await fetchAuthToken()
setWebViewUrl(buildWebViewUrl(token))
} catch (error) {
console.error("Error authenticating user:", error)
// Fall back to loading without a token if the user has an existing session
setWebViewUrl(buildWebViewUrl())
} finally {
setLoading(false)
}
}
const fetchAuthToken = async () => {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT)
try {
const response = await fetch(YOUR_BACKEND_AUTH_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
// Add your own auth headers here, e.g.:
// "Authorization": `Bearer ${yourUserSessionToken}`,
},
body: JSON.stringify({ id: userId }),
signal: controller.signal,
})
clearTimeout(timeoutId)
if (response.ok) {
const data = await response.json()
return data.token
} else {
const error = await response.json()
throw new Error(error.message || "Failed to authenticate")
}
} catch (error) {
clearTimeout(timeoutId)
throw error
}
}
const handleMessage = (event) => {
try {
const message = JSON.parse(event.nativeEvent.data)
switch (message.type) {
case MESSAGE_TYPES.CLOSE:
if (onClose) onClose()
break
case MESSAGE_TYPES.SESSION_EXPIRED:
handleSessionExpired()
break
default:
console.log("Unknown message type:", message.type)
}
} catch (error) {
console.error("Error handling message:", error)
}
}
const handleSessionExpired = async () => {
setLoading(true)
try {
const token = await fetchAuthToken()
setWebViewUrl(buildWebViewUrl(token))
} catch (error) {
console.error("Error refreshing session:", error)
Alert.alert(
"Session Refresh Failed",
"Unable to refresh your session. Please close and try again.",
[
{ text: "Close", onPress: onClose },
{ text: "Retry", onPress: handleSessionExpired },
]
)
} finally {
setLoading(false)
}
}
if (loading || !webViewUrl) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
)
}
return (
<Modal animationType="slide" transparent={false} visible={true} onRequestClose={onClose}>
<SafeAreaView style={styles.webview}>
<WebView
ref={webViewRef}
source={{ uri: webViewUrl }}
onMessage={handleMessage}
style={styles.webview}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
renderLoading={() => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
)}
/>
</SafeAreaView>
</Modal>
)
}
const styles = StyleSheet.create({
webview: { flex: 1 },
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#FFFFFF",
},
})
export default LearnInkWebView
import React, { useState, useRef, useEffect } from "react"
import { View, ActivityIndicator, StyleSheet, Alert, Modal } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"
import { WebView, WebViewMessageEvent } from "react-native-webview"
const YOUR_BACKEND_AUTH_URL = "https://your-api.com/auth/learnink"
const WEBVIEW_BASE_URL = "https://m.learn.ink"
const ORG_ID = "acme"
const REQUEST_TIMEOUT = 5000
const MESSAGE_TYPES = {
CLOSE: "CLOSE",
SESSION_EXPIRED: "SESSION_EXPIRED",
}
interface Props {
path: string
onClose: () => void
userId: string
}
const LearnInkWebView = ({ path, onClose, userId }: Props) => {
const [webViewUrl, setWebViewUrl] = useState<string | undefined>(undefined)
const [loading, setLoading] = useState<boolean>(true)
const webViewRef = useRef<WebView | null>(null)
useEffect(() => {
authenticateUser()
}, [userId])
const buildWebViewUrl = (token?: string) => {
const params = new URLSearchParams({
webview: "true",
...(token && { token }),
})
return `${WEBVIEW_BASE_URL}/${ORG_ID}/${path}?${params.toString()}`
}
const authenticateUser = async () => {
setLoading(true)
try {
const token = await fetchAuthToken()
setWebViewUrl(buildWebViewUrl(token))
} catch (error) {
console.error("Error authenticating user:", error)
setWebViewUrl(buildWebViewUrl())
} finally {
setLoading(false)
}
}
const fetchAuthToken = async () => {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT)
try {
const response = await fetch(YOUR_BACKEND_AUTH_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
// "Authorization": `Bearer ${yourUserSessionToken}`,
},
body: JSON.stringify({ id: userId }),
signal: controller.signal,
})
clearTimeout(timeoutId)
if (response.ok) {
const data = await response.json()
return data.token
} else {
const error = await response.json()
throw new Error(error.message || "Failed to authenticate")
}
} catch (error) {
clearTimeout(timeoutId)
throw error
}
}
const handleMessage = (event: WebViewMessageEvent) => {
try {
const message = JSON.parse(event.nativeEvent.data)
switch (message.type) {
case MESSAGE_TYPES.CLOSE:
if (onClose) onClose()
break
case MESSAGE_TYPES.SESSION_EXPIRED:
handleSessionExpired()
break
default:
console.log("Unknown message type:", message.type)
}
} catch (error) {
console.error("Error handling message:", error)
}
}
const handleSessionExpired = async () => {
setLoading(true)
try {
const token = await fetchAuthToken()
setWebViewUrl(buildWebViewUrl(token))
} catch (error) {
console.error("Error refreshing session:", error)
Alert.alert(
"Session Refresh Failed",
"Unable to refresh your session. Please close and try again.",
[
{ text: "Close", onPress: onClose },
{ text: "Retry", onPress: handleSessionExpired },
]
)
} finally {
setLoading(false)
}
}
if (loading || !webViewUrl) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
)
}
return (
<Modal animationType="slide" transparent={false} visible={true} onRequestClose={onClose}>
<SafeAreaView style={styles.webview}>
<WebView
ref={webViewRef}
source={{ uri: webViewUrl }}
onMessage={handleMessage}
style={styles.webview}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
renderLoading={() => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
)}
/>
</SafeAreaView>
</Modal>
)
}
const styles = StyleSheet.create({
webview: { flex: 1 },
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#FFFFFF",
},
})
export default LearnInkWebView