Add audio to your mobile app
Integrate Everlit audio players into iOS, Android, React Native, and Flutter apps using WebView components.
Overview
This guide shows you how to add Everlit audio to native mobile apps using WebView components. Whether you're using React Native, native iOS, native Android, or Flutter, the approach is the same: load a WebView with the Everlit widget HTML.
How It Works
Mobile integration uses a WebView to display the Everlit audio player:
- Create a WebView in your native app
- Load HTML containing your article content and the Everlit widget
- The widget automatically converts content to audio and displays the player
Platform Examples
Here's how to create a WebView and load the HTML snippet in different platforms:
Installation
npm install react-native-webview
# or
yarn add react-native-webview
Basic Implementation
import React, { useRef } from 'react';
import { WebView } from 'react-native-webview';
const ArticleWithAudio = ({ articleUrl, publicationId }) => {
const webViewRef = useRef(null);
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
margin: 0;
padding: 16px;
font-family: -apple-system, sans-serif;
}
#everlit-audio-embed-container iframe {
width: 100%;
border: none;
}
</style>
</head>
<body>
<div id="article-content">
<h1>Article Title</h1>
<div class="articleBody">
<p>Article content...</p>
</div>
</div>
<!-- Everlit Audio Widget -->
<div
id="everlit-auto-audio-widget"
data-publication-id="${publicationId}"
style="height: 136px; width: 100%;"
hidden
></div>
<script
defer
src="https://cdn.everlit.audio/libs/everlitAutoAudio.js"
type="text/javascript"
></script>
</body>
</html>
`;
return (
<WebView
ref={webViewRef}
source={{ html: htmlContent }}
style={{ flex: 1 }}
javaScriptEnabled={true}
domStorageEnabled={true}
thirdPartyCookiesEnabled={true}
sharedCookiesEnabled={true}
originWhitelist={['*']}
allowsInlineMediaPlayback={true}
mediaPlaybackRequiresUserAction={false}
mixedContentMode="always"
/>
);
};
export default ArticleWithAudio;
Swift Implementation
import WebKit
class ArticleViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let htmlString = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { margin: 0; padding: 16px; font-family: -apple-system; }
#everlit-audio-embed-container iframe { width: 100%; border: none; }
</style>
</head>
<body>
<h1>Article Title</h1>
<div class="articleBody">
<p>Article content...</p>
</div>
<div
id="everlit-auto-audio-widget"
data-publication-id="YOUR-PUBLICATION-ID"
style="height: 136px; width: 100%;"
hidden
></div>
<script defer src="https://cdn.everlit.audio/libs/everlitAutoAudio.js"></script>
</body>
</html>
"""
webView.loadHTMLString(htmlString, baseURL: nil)
}
}
Kotlin Implementation
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
class ArticleActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this)
setContentView(webView)
// Enable JavaScript
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
// Set WebView client
webView.webViewClient = WebViewClient()
val htmlContent = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { margin: 0; padding: 16px; font-family: sans-serif; }
#everlit-audio-embed-container iframe { width: 100%; border: none; }
</style>
</head>
<body>
<h1>Article Title</h1>
<div class="articleBody">
<p>Article content...</p>
</div>
<div
id="everlit-auto-audio-widget"
data-publication-id="YOUR-PUBLICATION-ID"
style="height: 136px; width: 100%;"
hidden
></div>
<script defer src="https://cdn.everlit.audio/libs/everlitAutoAudio.js"></script>
</body>
</html>
""".trimIndent()
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null)
}
}
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
pubspec.yaml
dependencies:
webview_flutter: ^4.4.0
Dart Implementation
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class ArticleScreen extends StatefulWidget {
@override
_ArticleScreenState createState() => _ArticleScreenState();
}
class _ArticleScreenState extends State<ArticleScreen> {
late final WebViewController controller;
@override
void initState() {
super.initState();
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadHtmlString('''
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { margin: 0; padding: 16px; font-family: sans-serif; }
#everlit-audio-embed-container iframe { width: 100%; border: none; }
</style>
</head>
<body>
<h1>Article Title</h1>
<div class="articleBody">
<p>Article content...</p>
</div>
<div
id="everlit-auto-audio-widget"
data-publication-id="YOUR-PUBLICATION-ID"
style="height: 136px; width: 100%;"
hidden
></div>
<script defer src="https://cdn.everlit.audio/libs/everlitAutoAudio.js"></script>
</body>
</html>
''');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Article')),
body: WebViewWidget(controller: controller),
);
}
}
Advanced Options
The basic WebView approach above works out of the box. For more control—custom loading states, error handling, or reliable EUT (analytics token) persistence across app sessions—call the Auto Audio API directly from your app and render the returned iframe player in a WebView.
Direct API Integration
This approach gives you programmatic control over API calls, error handling, and conversion status, while still rendering the returned iframe player.
check and create calls shown below
don't require a token. A bearer token (Authorization: Bearer api_…) is only
needed for server-to-server content ingestion (sending article text directly). API tokens are
scoped to your publication(s)—request one from Everlit. Avoid shipping a token inside an app
binary you distribute to users.
Installation
npm install @react-native-async-storage/async-storage
# or
yarn add @react-native-async-storage/async-storage
API Client Implementation
// everlitAudioClient.js
import AsyncStorage from '@react-native-async-storage/async-storage';
class EverlitAudioClient {
constructor(publicationId, apiToken = null) {
this.publicationId = publicationId;
this.apiToken = apiToken;
this.baseUrl = 'https://creator.everlit.audio';
}
async generateEUT() {
const now = Date.now();
const timeHex = now.toString(16).padStart(12, '0');
const randomBytes = new Uint8Array(10);
for (let i = 0; i < 10; i++) {
randomBytes[i] = Math.floor(Math.random() * 256);
}
const randomHex = Array.from(randomBytes)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
return `${timeHex.slice(0, 8)}${timeHex.slice(8)}7${randomHex.slice(0, 3)}${randomHex.slice(3, 5)}${randomHex.slice(5, 15)}`;
}
async ensureEUT() {
let eut = await AsyncStorage.getItem('everlit_eut');
if (!eut) {
eut = await this.generateEUT();
await AsyncStorage.setItem('everlit_eut', eut);
}
return eut;
}
encodeEVP(params) {
const json = JSON.stringify(params);
return btoa(json)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
async checkStatus(url, contentData = null) {
const eut = await this.ensureEUT();
const params = { url, publication_id: this.publicationId, eut };
if (contentData) Object.assign(params, contentData);
const evp = this.encodeEVP(params);
const response = await fetch(
`${this.baseUrl}/audio/auto/check?evp=${evp}`
);
return await response.json();
}
async createAudio(url, options = {}) {
const eut = await this.ensureEUT();
const headers = { 'Content-Type': 'application/json' };
if (this.apiToken) {
headers['Authorization'] = `Bearer ${this.apiToken}`;
}
const response = await fetch(`${this.baseUrl}/audio/auto`, {
method: 'POST',
headers,
body: JSON.stringify({
url,
publication_id: this.publicationId,
eut,
...options,
}),
});
return await response.json();
}
async getOrCreateAudio(url, options = {}) {
const checkResult = await this.checkStatus(url);
if (checkResult.embed) return checkResult;
if (checkResult.create || checkResult.click_to_create) {
const createResult = await this.createAudio(url, options);
if (createResult.embed) return createResult;
if (createResult.waiting) {
return await this.pollForCompletion(url);
}
return createResult;
}
if (checkResult.waiting) {
return await this.pollForCompletion(url);
}
return checkResult;
}
async pollForCompletion(url, maxAttempts = 60, delay = 5000) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
await new Promise(resolve => setTimeout(resolve, delay));
const status = await this.checkStatus(url);
if (status.embed) return status;
if (!status.successful && !status.waiting) {
throw new Error(status.reason || 'Conversion failed');
}
}
throw new Error('Conversion timeout');
}
extractIframeUrl(embedHtml) {
const match = embedHtml.match(/src="([^"]+)"/);
return match ? match[1] : null;
}
}
export default EverlitAudioClient;
// ArticleAudioPlayer.js
import React, { useState, useEffect } from 'react';
import { View, ActivityIndicator, Text, StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';
import EverlitAudioClient from './everlitAudioClient';
const ArticleAudioPlayer = ({
articleUrl,
publicationId,
apiToken = null,
onStatusChange = () => {},
}) => {
const [status, setStatus] = useState('loading');
const [embedUrl, setEmbedUrl] = useState(null);
const [error, setError] = useState(null);
const [client] = useState(() => new EverlitAudioClient(publicationId, apiToken));
useEffect(() => {
loadAudio();
}, [articleUrl]);
const loadAudio = async () => {
try {
setStatus('loading');
setError(null);
onStatusChange('loading');
const result = await client.getOrCreateAudio(articleUrl);
if (result.embed) {
const url = client.extractIframeUrl(result.embed);
setEmbedUrl(url);
setStatus('ready');
onStatusChange('ready', result);
} else if (result.click_to_create) {
setStatus('click_to_create');
onStatusChange('click_to_create', result);
} else if (!result.successful) {
throw new Error(result.reason || 'Unknown error');
}
} catch (err) {
setError(err.message);
setStatus('error');
onStatusChange('error', err);
}
};
if (status === 'loading') {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#0066CC" />
<Text style={styles.statusText}>Checking for audio...</Text>
</View>
);
}
if (status === 'ready' && embedUrl) {
return (
<View style={styles.playerContainer}>
<WebView
source={{ uri: embedUrl }}
style={styles.webview}
scrollEnabled={false}
allowsInlineMediaPlayback={true}
mediaPlaybackRequiresUserAction={false}
/>
</View>
);
}
return null;
};
const styles = StyleSheet.create({
container: {
padding: 20,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F5F5F5',
borderRadius: 8,
minHeight: 130,
},
playerContainer: {
height: 130,
backgroundColor: '#F5F5F5',
borderRadius: 8,
overflow: 'hidden',
},
webview: {
flex: 1,
backgroundColor: 'transparent',
},
statusText: {
marginTop: 12,
fontSize: 14,
color: '#666',
},
});
export default ArticleAudioPlayer;
// App.js or your article screen
import React from 'react';
import { ScrollView, Text } from 'react-native';
import ArticleAudioPlayer from './ArticleAudioPlayer';
const ArticleScreen = () => {
return (
<ScrollView style={{ flex: 1, padding: 16 }}>
<Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 16 }}>
Article Title
</Text>
{/* Audio Player */}
<ArticleAudioPlayer
articleUrl="https://example.com/article-slug"
publicationId="pblc_your_publication_id"
onStatusChange={(status, data) => {
console.log('Audio status:', status, data);
}}
/>
{/* Article Content */}
<Text style={{ marginTop: 16, fontSize: 16, lineHeight: 24 }}>
Your article content here...
</Text>
</ScrollView>
);
};
export default ArticleScreen;
Mobile Considerations
Cookie Storage
Problem: Mobile WebViews may not persist cookies reliably.
Solution: Use the Direct API approach and manage the EUT yourself (e.g. AsyncStorage), passing it on each request.
Iframe Rendering
Problem: Android WebView versions have issues with nested iframes.
Solution: Set allowsInlineMediaPlayback and mixedContentMode="always".
CORS and Security
Problem: WebView may block cross-origin requests.
Solution: Set originWhitelist={['*']} and enable third-party cookies.
Content Extraction
Problem: Widget expects specific DOM structures.
Solution: Use native API and pass extracted content directly.
Performance
Problem: Full WebView + iframe can be slow.
Solution: Use native API integration or lazy load the player.
Audio Playback
Problem: iOS requires user interaction for audio.
Solution: Set mediaPlaybackRequiresUserAction={false}.
WebView Configuration Reference
<WebView
javaScriptEnabled={true}
domStorageEnabled={true}
thirdPartyCookiesEnabled={true}
sharedCookiesEnabled={true}
originWhitelist={['*']}
allowsInlineMediaPlayback={true}
mediaPlaybackRequiresUserAction={false}
mixedContentMode="always" // Android
/>
Troubleshooting
Common Issues
| Issue | Possible Cause | Solution |
|---|---|---|
| Audio player not appearing | Incorrect publication ID or JavaScript disabled | Verify data-publication-id and javaScriptEnabled={true} |
| Cookies not persisting | WebView cookie restrictions | Use the Direct API approach and pass a stored EUT on each request |
| Iframe not rendering | WebView configuration | Set allowsInlineMediaPlayback={true} and originWhitelist={['*']} |
| Audio not playing | iOS autoplay restrictions | Set mediaPlaybackRequiresUserAction={false} |
| Content not extracted | Missing DOM structure | Use native API and pass extractedText explicitly |
Debug WebView
Enable remote debugging to see console errors:
<WebView
onError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
console.error('WebView error:', nativeEvent);
}}
onHttpError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
console.error('HTTP error:', nativeEvent);
}}
onMessage={(event) => {
console.log('WebView message:', event.nativeEvent.data);
}}
/>
iOS: Safari > Develop > [Your Device] > [Your App]
Android: chrome://inspect/#devices