@parastud/floating-bubble 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/android/build.gradle +19 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/ai/rastaa/floatingbubble/FloatingBubbleModule.kt +206 -0
- package/build/index.d.ts +50 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +48 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/FloatingBubble.podspec +20 -0
- package/ios/FloatingBubbleModule.swift +30 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @parastud/floating-bubble
|
|
2
|
+
|
|
3
|
+
A draggable, circular **"back to Rastaa"** bubble that floats on top of other apps
|
|
4
|
+
(Ola/Porter style), so a driver can tap it to jump straight back into the app while
|
|
5
|
+
a navigation app (e.g. Google Maps) is in the foreground.
|
|
6
|
+
|
|
7
|
+
- **Android** — a real overlay drawn with `WindowManager`, gated on the
|
|
8
|
+
`SYSTEM_ALERT_WINDOW` ("Display over other apps") permission.
|
|
9
|
+
- **iOS** — there is no public API to draw over other apps, so the native module is
|
|
10
|
+
a **no-op stub**. Every method resolves to a safe default; nothing crashes.
|
|
11
|
+
|
|
12
|
+
This is a native Expo module: it ships native Android/iOS code and **requires a
|
|
13
|
+
native rebuild** (dev client or EAS build). It cannot be delivered as an OTA update.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @parastud/floating-bubble
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then rebuild the native app (autolinking picks the module up automatically):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx expo run:android # or: eas build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The Android `SYSTEM_ALERT_WINDOW` permission is merged into the host app's manifest
|
|
28
|
+
automatically via this module's `AndroidManifest.xml`.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { FloatingBubble } from '@parastud/floating-bubble';
|
|
34
|
+
|
|
35
|
+
// Is the bubble actually usable here? (Android + native module present)
|
|
36
|
+
if (FloatingBubble.isSupported) {
|
|
37
|
+
// Has the user granted "Display over other apps"?
|
|
38
|
+
if (!FloatingBubble.canDrawOverlays()) {
|
|
39
|
+
FloatingBubble.requestPermission(); // opens the system settings screen
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
FloatingBubble.show(); // float the bubble over other apps
|
|
43
|
+
FloatingBubble.hide(); // remove it
|
|
44
|
+
FloatingBubble.isVisible();
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Every method is safe to call on any platform — on iOS, or on a build without the
|
|
49
|
+
native module, calls short-circuit to safe defaults instead of throwing.
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
| Member | Returns | Description |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `isSupported` | `boolean` | `true` only on Android with the native module present. |
|
|
56
|
+
| `canDrawOverlays()` | `boolean` | Whether "Display over other apps" is granted. |
|
|
57
|
+
| `requestPermission()` | `void` | Opens the system settings screen to grant the permission. |
|
|
58
|
+
| `isVisible()` | `boolean` | Whether the bubble is currently on screen. |
|
|
59
|
+
| `show()` | `void` | Show the bubble (no-op without permission / if already visible). |
|
|
60
|
+
| `hide()` | `void` | Remove the bubble. |
|
|
61
|
+
|
|
62
|
+
The raw native surface is also exported as the `FloatingBubbleNativeModule` type for
|
|
63
|
+
advanced use.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
UNLICENSED — internal Rastaa package.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'org.jetbrains.kotlin.android'
|
|
3
|
+
|
|
4
|
+
group = 'ai.rastaa.floatingbubble'
|
|
5
|
+
version = '0.1.0'
|
|
6
|
+
|
|
7
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
8
|
+
apply from: expoModulesCorePlugin
|
|
9
|
+
applyKotlinExpoModulesCorePlugin()
|
|
10
|
+
useCoreDependencies()
|
|
11
|
+
useDefaultAndroidSdkVersions()
|
|
12
|
+
useExpoPublishing()
|
|
13
|
+
|
|
14
|
+
android {
|
|
15
|
+
namespace "ai.rastaa.floatingbubble"
|
|
16
|
+
defaultConfig {
|
|
17
|
+
minSdkVersion 24
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
package ai.rastaa.floatingbubble
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.graphics.Outline
|
|
7
|
+
import android.graphics.PixelFormat
|
|
8
|
+
import android.graphics.drawable.GradientDrawable
|
|
9
|
+
import android.net.Uri
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import android.os.Handler
|
|
12
|
+
import android.os.Looper
|
|
13
|
+
import android.provider.Settings
|
|
14
|
+
import android.view.Gravity
|
|
15
|
+
import android.view.MotionEvent
|
|
16
|
+
import android.view.View
|
|
17
|
+
import android.view.ViewOutlineProvider
|
|
18
|
+
import android.view.WindowManager
|
|
19
|
+
import android.widget.FrameLayout
|
|
20
|
+
import android.widget.ImageView
|
|
21
|
+
import expo.modules.kotlin.modules.Module
|
|
22
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
23
|
+
import kotlin.math.abs
|
|
24
|
+
|
|
25
|
+
// Renders a draggable, circular "back to Rastaa" bubble on top of every other app
|
|
26
|
+
// (Ola/Porter style). Android only — iOS has no API to draw over other apps.
|
|
27
|
+
class FloatingBubbleModule : Module() {
|
|
28
|
+
|
|
29
|
+
private var bubbleView: View? = null
|
|
30
|
+
private var windowManager: WindowManager? = null
|
|
31
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
32
|
+
|
|
33
|
+
private val appContextOrNull: Context?
|
|
34
|
+
get() = appContext.reactContext?.applicationContext
|
|
35
|
+
|
|
36
|
+
override fun definition() = ModuleDefinition {
|
|
37
|
+
Name("FloatingBubble")
|
|
38
|
+
|
|
39
|
+
// Has the user granted "Display over other apps"?
|
|
40
|
+
Function("canDrawOverlays") {
|
|
41
|
+
val ctx = appContextOrNull ?: return@Function false
|
|
42
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) Settings.canDrawOverlays(ctx) else true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Send the user to the system settings screen to grant the overlay permission.
|
|
46
|
+
Function("requestOverlayPermission") {
|
|
47
|
+
val ctx = appContextOrNull
|
|
48
|
+
if (
|
|
49
|
+
ctx != null &&
|
|
50
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
|
51
|
+
!Settings.canDrawOverlays(ctx)
|
|
52
|
+
) {
|
|
53
|
+
val intent = Intent(
|
|
54
|
+
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
|
55
|
+
Uri.parse("package:${ctx.packageName}"),
|
|
56
|
+
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
57
|
+
ctx.startActivity(intent)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Function("isVisible") { bubbleView != null }
|
|
62
|
+
|
|
63
|
+
Function("show") {
|
|
64
|
+
mainHandler.post { showBubble() }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Function("hide") {
|
|
68
|
+
mainHandler.post { hideBubble() }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
OnDestroy {
|
|
72
|
+
mainHandler.post { hideBubble() }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun showBubble() {
|
|
77
|
+
val ctx = appContextOrNull ?: return
|
|
78
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(ctx)) return
|
|
79
|
+
if (bubbleView != null) return
|
|
80
|
+
|
|
81
|
+
val wm = ctx.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
|
82
|
+
windowManager = wm
|
|
83
|
+
|
|
84
|
+
val density = ctx.resources.displayMetrics.density
|
|
85
|
+
val sizePx = (60 * density).toInt()
|
|
86
|
+
val padPx = (8 * density).toInt()
|
|
87
|
+
|
|
88
|
+
// Black circle with the app icon centered inside it.
|
|
89
|
+
val container = FrameLayout(ctx).apply {
|
|
90
|
+
background = GradientDrawable().apply {
|
|
91
|
+
shape = GradientDrawable.OVAL
|
|
92
|
+
setColor(Color.BLACK)
|
|
93
|
+
}
|
|
94
|
+
elevation = 12 * density
|
|
95
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
96
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
97
|
+
outline.setOval(0, 0, view.width, view.height)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
clipToOutline = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
val icon = ImageView(ctx).apply {
|
|
104
|
+
scaleType = ImageView.ScaleType.CENTER_CROP
|
|
105
|
+
try {
|
|
106
|
+
setImageDrawable(ctx.packageManager.getApplicationIcon(ctx.packageName))
|
|
107
|
+
} catch (_: Exception) {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
container.addView(
|
|
111
|
+
icon,
|
|
112
|
+
FrameLayout.LayoutParams(sizePx - padPx, sizePx - padPx, Gravity.CENTER),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
val windowType =
|
|
116
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
|
117
|
+
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
|
118
|
+
else
|
|
119
|
+
@Suppress("DEPRECATION")
|
|
120
|
+
WindowManager.LayoutParams.TYPE_PHONE
|
|
121
|
+
|
|
122
|
+
val params = WindowManager.LayoutParams(
|
|
123
|
+
sizePx,
|
|
124
|
+
sizePx,
|
|
125
|
+
windowType,
|
|
126
|
+
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
|
127
|
+
PixelFormat.TRANSLUCENT,
|
|
128
|
+
).apply {
|
|
129
|
+
gravity = Gravity.TOP or Gravity.START
|
|
130
|
+
x = (16 * density).toInt()
|
|
131
|
+
y = (140 * density).toInt()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
container.setOnTouchListener(object : View.OnTouchListener {
|
|
135
|
+
private var startX = 0
|
|
136
|
+
private var startY = 0
|
|
137
|
+
private var touchDownX = 0f
|
|
138
|
+
private var touchDownY = 0f
|
|
139
|
+
private var dragging = false
|
|
140
|
+
private val touchSlop = 8 * density
|
|
141
|
+
|
|
142
|
+
override fun onTouch(v: View, e: MotionEvent): Boolean {
|
|
143
|
+
when (e.action) {
|
|
144
|
+
MotionEvent.ACTION_DOWN -> {
|
|
145
|
+
startX = params.x
|
|
146
|
+
startY = params.y
|
|
147
|
+
touchDownX = e.rawX
|
|
148
|
+
touchDownY = e.rawY
|
|
149
|
+
dragging = false
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
MotionEvent.ACTION_MOVE -> {
|
|
153
|
+
val dx = e.rawX - touchDownX
|
|
154
|
+
val dy = e.rawY - touchDownY
|
|
155
|
+
if (abs(dx) > touchSlop || abs(dy) > touchSlop) dragging = true
|
|
156
|
+
params.x = startX + dx.toInt()
|
|
157
|
+
params.y = startY + dy.toInt()
|
|
158
|
+
try {
|
|
159
|
+
wm.updateViewLayout(v, params)
|
|
160
|
+
} catch (_: Exception) {
|
|
161
|
+
}
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
MotionEvent.ACTION_UP -> {
|
|
165
|
+
if (!dragging) bringAppToFront()
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
wm.addView(container, params)
|
|
175
|
+
bubbleView = container
|
|
176
|
+
} catch (_: Exception) {
|
|
177
|
+
bubbleView = null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private fun bringAppToFront() {
|
|
182
|
+
val ctx = appContextOrNull ?: return
|
|
183
|
+
val launchIntent = ctx.packageManager.getLaunchIntentForPackage(ctx.packageName)
|
|
184
|
+
launchIntent?.addFlags(
|
|
185
|
+
Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
186
|
+
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or
|
|
187
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP,
|
|
188
|
+
)
|
|
189
|
+
if (launchIntent != null) {
|
|
190
|
+
try {
|
|
191
|
+
ctx.startActivity(launchIntent)
|
|
192
|
+
} catch (_: Exception) {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
hideBubble()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private fun hideBubble() {
|
|
199
|
+
val view = bubbleView ?: return
|
|
200
|
+
try {
|
|
201
|
+
windowManager?.removeView(view)
|
|
202
|
+
} catch (_: Exception) {
|
|
203
|
+
}
|
|
204
|
+
bubbleView = null
|
|
205
|
+
}
|
|
206
|
+
}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The raw native surface registered by the module on each platform.
|
|
3
|
+
*
|
|
4
|
+
* On Android these are backed by a real `WindowManager` overlay drawn with the
|
|
5
|
+
* SYSTEM_ALERT_WINDOW ("Display over other apps") permission. iOS has no public
|
|
6
|
+
* API to draw over other apps, so the native module there is a no-op stub and
|
|
7
|
+
* every method returns a safe default.
|
|
8
|
+
*
|
|
9
|
+
* Prefer the {@link FloatingBubble} façade below — it guards every call so it is
|
|
10
|
+
* safe to invoke on any platform.
|
|
11
|
+
*/
|
|
12
|
+
export type FloatingBubbleNativeModule = {
|
|
13
|
+
/** Whether the user has granted "Display over other apps" (SYSTEM_ALERT_WINDOW). */
|
|
14
|
+
canDrawOverlays(): boolean;
|
|
15
|
+
/** Opens the system settings screen to grant the overlay permission. */
|
|
16
|
+
requestOverlayPermission(): void;
|
|
17
|
+
/** Whether the bubble is currently on screen. */
|
|
18
|
+
isVisible(): boolean;
|
|
19
|
+
/** Show the bubble (no-op without permission, or if already visible). */
|
|
20
|
+
show(): void;
|
|
21
|
+
/** Remove the bubble. */
|
|
22
|
+
hide(): void;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* True only where the bubble can actually render: Android, with the native
|
|
26
|
+
* module present in the running binary. Everywhere else every call below is a
|
|
27
|
+
* safe no-op.
|
|
28
|
+
*/
|
|
29
|
+
export declare const isSupported: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Cross-platform façade over the native module. Safe to call on any platform —
|
|
32
|
+
* on iOS, or on a build without the native module, methods short-circuit to safe
|
|
33
|
+
* defaults instead of throwing.
|
|
34
|
+
*/
|
|
35
|
+
export declare const FloatingBubble: {
|
|
36
|
+
/** See {@link isSupported}. */
|
|
37
|
+
isSupported: boolean;
|
|
38
|
+
/** Whether the user has granted "Display over other apps". `false` if unsupported. */
|
|
39
|
+
canDrawOverlays(): boolean;
|
|
40
|
+
/** Send the user to the system settings screen to grant the overlay permission. */
|
|
41
|
+
requestPermission(): void;
|
|
42
|
+
/** Whether the bubble is currently on screen. `false` if unsupported. */
|
|
43
|
+
isVisible(): boolean;
|
|
44
|
+
/** Show the bubble. No-op without permission, if already visible, or if unsupported. */
|
|
45
|
+
show(): void;
|
|
46
|
+
/** Remove the bubble. No-op if unsupported. */
|
|
47
|
+
hide(): void;
|
|
48
|
+
};
|
|
49
|
+
export default FloatingBubble;
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,oFAAoF;IACpF,eAAe,IAAI,OAAO,CAAC;IAC3B,wEAAwE;IACxE,wBAAwB,IAAI,IAAI,CAAC;IACjC,iDAAiD;IACjD,SAAS,IAAI,OAAO,CAAC;IACrB,yEAAyE;IACzE,IAAI,IAAI,IAAI,CAAC;IACb,yBAAyB;IACzB,IAAI,IAAI,IAAI,CAAC;CACd,CAAC;AAWF;;;;GAIG;AACH,eAAO,MAAM,WAAW,SAA8B,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,+BAA+B;;IAG/B,sFAAsF;uBACnE,OAAO;IAI1B,mFAAmF;yBAC9D,IAAI;IAIzB,yEAAyE;iBAC5D,OAAO;IAIpB,wFAAwF;YAChF,IAAI;IAIZ,+CAA+C;YACvC,IAAI;CAGb,CAAC;AAEF,eAAe,cAAc,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { requireOptionalNativeModule } from 'expo';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
const isAndroid = Platform.OS === 'android';
|
|
4
|
+
// `requireOptionalNativeModule` returns null instead of throwing when the native
|
|
5
|
+
// module isn't in the running binary. That keeps a JS-only/OTA bundle from
|
|
6
|
+
// crashing if it ever lands on an older build that predates this native module —
|
|
7
|
+
// the bubble simply does nothing there. iOS ships a no-op stub of the same name.
|
|
8
|
+
const native = requireOptionalNativeModule('FloatingBubble');
|
|
9
|
+
/**
|
|
10
|
+
* True only where the bubble can actually render: Android, with the native
|
|
11
|
+
* module present in the running binary. Everywhere else every call below is a
|
|
12
|
+
* safe no-op.
|
|
13
|
+
*/
|
|
14
|
+
export const isSupported = isAndroid && native != null;
|
|
15
|
+
/**
|
|
16
|
+
* Cross-platform façade over the native module. Safe to call on any platform —
|
|
17
|
+
* on iOS, or on a build without the native module, methods short-circuit to safe
|
|
18
|
+
* defaults instead of throwing.
|
|
19
|
+
*/
|
|
20
|
+
export const FloatingBubble = {
|
|
21
|
+
/** See {@link isSupported}. */
|
|
22
|
+
isSupported,
|
|
23
|
+
/** Whether the user has granted "Display over other apps". `false` if unsupported. */
|
|
24
|
+
canDrawOverlays() {
|
|
25
|
+
return isSupported ? native.canDrawOverlays() : false;
|
|
26
|
+
},
|
|
27
|
+
/** Send the user to the system settings screen to grant the overlay permission. */
|
|
28
|
+
requestPermission() {
|
|
29
|
+
if (isSupported)
|
|
30
|
+
native.requestOverlayPermission();
|
|
31
|
+
},
|
|
32
|
+
/** Whether the bubble is currently on screen. `false` if unsupported. */
|
|
33
|
+
isVisible() {
|
|
34
|
+
return isSupported ? native.isVisible() : false;
|
|
35
|
+
},
|
|
36
|
+
/** Show the bubble. No-op without permission, if already visible, or if unsupported. */
|
|
37
|
+
show() {
|
|
38
|
+
if (isSupported)
|
|
39
|
+
native.show();
|
|
40
|
+
},
|
|
41
|
+
/** Remove the bubble. No-op if unsupported. */
|
|
42
|
+
hide() {
|
|
43
|
+
if (isSupported)
|
|
44
|
+
native.hide();
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
export default FloatingBubble;
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,MAAM,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AA0BxC,MAAM,SAAS,GAAG,QAAQ,CAAC,EAAE,KAAK,SAAS,CAAC;AAE5C,iFAAiF;AACjF,2EAA2E;AAC3E,iFAAiF;AACjF,iFAAiF;AACjF,MAAM,MAAM,GACV,2BAA2B,CAA6B,gBAAgB,CAAC,CAAC;AAE5E;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,IAAI,MAAM,IAAI,IAAI,CAAC;AAEvD;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,+BAA+B;IAC/B,WAAW;IAEX,sFAAsF;IACtF,eAAe;QACb,OAAO,WAAW,CAAC,CAAC,CAAC,MAAO,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACzD,CAAC;IAED,mFAAmF;IACnF,iBAAiB;QACf,IAAI,WAAW;YAAE,MAAO,CAAC,wBAAwB,EAAE,CAAC;IACtD,CAAC;IAED,yEAAyE;IACzE,SAAS;QACP,OAAO,WAAW,CAAC,CAAC,CAAC,MAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACnD,CAAC;IAED,wFAAwF;IACxF,IAAI;QACF,IAAI,WAAW;YAAE,MAAO,CAAC,IAAI,EAAE,CAAC;IAClC,CAAC;IAED,+CAA+C;IAC/C,IAAI;QACF,IAAI,WAAW;YAAE,MAAO,CAAC,IAAI,EAAE,CAAC;IAClC,CAAC;CACF,CAAC;AAEF,eAAe,cAAc,CAAC","sourcesContent":["import { requireOptionalNativeModule } from 'expo';\nimport { Platform } from 'react-native';\n\n/**\n * The raw native surface registered by the module on each platform.\n *\n * On Android these are backed by a real `WindowManager` overlay drawn with the\n * SYSTEM_ALERT_WINDOW (\"Display over other apps\") permission. iOS has no public\n * API to draw over other apps, so the native module there is a no-op stub and\n * every method returns a safe default.\n *\n * Prefer the {@link FloatingBubble} façade below — it guards every call so it is\n * safe to invoke on any platform.\n */\nexport type FloatingBubbleNativeModule = {\n /** Whether the user has granted \"Display over other apps\" (SYSTEM_ALERT_WINDOW). */\n canDrawOverlays(): boolean;\n /** Opens the system settings screen to grant the overlay permission. */\n requestOverlayPermission(): void;\n /** Whether the bubble is currently on screen. */\n isVisible(): boolean;\n /** Show the bubble (no-op without permission, or if already visible). */\n show(): void;\n /** Remove the bubble. */\n hide(): void;\n};\n\nconst isAndroid = Platform.OS === 'android';\n\n// `requireOptionalNativeModule` returns null instead of throwing when the native\n// module isn't in the running binary. That keeps a JS-only/OTA bundle from\n// crashing if it ever lands on an older build that predates this native module —\n// the bubble simply does nothing there. iOS ships a no-op stub of the same name.\nconst native =\n requireOptionalNativeModule<FloatingBubbleNativeModule>('FloatingBubble');\n\n/**\n * True only where the bubble can actually render: Android, with the native\n * module present in the running binary. Everywhere else every call below is a\n * safe no-op.\n */\nexport const isSupported = isAndroid && native != null;\n\n/**\n * Cross-platform façade over the native module. Safe to call on any platform —\n * on iOS, or on a build without the native module, methods short-circuit to safe\n * defaults instead of throwing.\n */\nexport const FloatingBubble = {\n /** See {@link isSupported}. */\n isSupported,\n\n /** Whether the user has granted \"Display over other apps\". `false` if unsupported. */\n canDrawOverlays(): boolean {\n return isSupported ? native!.canDrawOverlays() : false;\n },\n\n /** Send the user to the system settings screen to grant the overlay permission. */\n requestPermission(): void {\n if (isSupported) native!.requestOverlayPermission();\n },\n\n /** Whether the bubble is currently on screen. `false` if unsupported. */\n isVisible(): boolean {\n return isSupported ? native!.isVisible() : false;\n },\n\n /** Show the bubble. No-op without permission, if already visible, or if unsupported. */\n show(): void {\n if (isSupported) native!.show();\n },\n\n /** Remove the bubble. No-op if unsupported. */\n hide(): void {\n if (isSupported) native!.hide();\n },\n};\n\nexport default FloatingBubble;\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'FloatingBubble'
|
|
3
|
+
s.version = '0.1.0'
|
|
4
|
+
s.summary = 'Back-to-Rastaa floating bubble (Android-only; iOS no-op stub).'
|
|
5
|
+
s.description = 'Expo module that draws a return-to-app bubble over other apps on Android.'
|
|
6
|
+
s.author = 'Rastaa <tech@rastaa.ai>'
|
|
7
|
+
s.homepage = 'https://github.com/rastaa/floating-bubble'
|
|
8
|
+
s.platforms = { :ios => '15.1', :tvos => '15.1' }
|
|
9
|
+
s.source = { git: 'https://github.com/rastaa/floating-bubble.git', tag: "v#{s.version}" }
|
|
10
|
+
s.static_framework = true
|
|
11
|
+
|
|
12
|
+
s.dependency 'ExpoModulesCore'
|
|
13
|
+
|
|
14
|
+
s.pod_target_xcconfig = {
|
|
15
|
+
'DEFINES_MODULE' => 'YES',
|
|
16
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
20
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
// iOS cannot draw a widget over other apps (no public API), so this is a no-op
|
|
4
|
+
// stub. It exists only so `requireNativeModule('FloatingBubble')` resolves and
|
|
5
|
+
// the JS layer can call the same API on both platforms without crashing.
|
|
6
|
+
public class FloatingBubbleModule: Module {
|
|
7
|
+
public func definition() -> ModuleDefinition {
|
|
8
|
+
Name("FloatingBubble")
|
|
9
|
+
|
|
10
|
+
Function("canDrawOverlays") { () -> Bool in
|
|
11
|
+
false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
Function("requestOverlayPermission") {
|
|
15
|
+
// No-op on iOS.
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Function("isVisible") { () -> Bool in
|
|
19
|
+
false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Function("show") {
|
|
23
|
+
// No-op on iOS.
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Function("hide") {
|
|
27
|
+
// No-op on iOS.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parastud/floating-bubble",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Back-to-Rastaa floating bubble — a draggable circular overlay to jump back into the app from a navigation app. Android only; iOS is a safe no-op stub.",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "expo-module build",
|
|
9
|
+
"clean": "expo-module clean",
|
|
10
|
+
"lint": "expo-module lint",
|
|
11
|
+
"test": "expo-module test",
|
|
12
|
+
"prepare": "expo-module prepare",
|
|
13
|
+
"prepublishOnly": "expo-module prepublishOnly",
|
|
14
|
+
"expo-module": "expo-module"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react-native",
|
|
18
|
+
"expo",
|
|
19
|
+
"expo-module",
|
|
20
|
+
"floating-bubble",
|
|
21
|
+
"overlay",
|
|
22
|
+
"system-alert-window",
|
|
23
|
+
"android"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/parastud/floating-bubble.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/parastud/floating-bubble/issues"
|
|
31
|
+
},
|
|
32
|
+
"author": "Rastaa <tech@rastaa.ai>",
|
|
33
|
+
"license": "UNLICENSED",
|
|
34
|
+
"homepage": "https://github.com/parastud/floating-bubble#readme",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"build",
|
|
40
|
+
"android",
|
|
41
|
+
"ios",
|
|
42
|
+
"expo-module.config.json",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"expo": "*",
|
|
47
|
+
"react": "*",
|
|
48
|
+
"react-native": "*"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/react": "~19.2.10",
|
|
52
|
+
"expo": "~55.0.26",
|
|
53
|
+
"expo-module-scripts": "^55.0.0",
|
|
54
|
+
"expo-modules-core": "~55.0.25",
|
|
55
|
+
"react": "19.2.0",
|
|
56
|
+
"react-native": "0.83.6",
|
|
57
|
+
"typescript": "~5.9.2"
|
|
58
|
+
}
|
|
59
|
+
}
|