sms-read-plugin-android 0.0.1
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/Package.swift +28 -0
- package/README.md +173 -0
- package/SmsReadPluginAndroid.podspec +17 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/gokulnaryanan/voltpath/sms/SmsReadPlugin.java +50 -0
- package/android/src/main/java/com/gokulnaryanan/voltpath/sms/SmsReadPluginPlugin.java +227 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +238 -0
- package/dist/esm/definitions.d.ts +80 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +6 -0
- package/dist/esm/web.js +10 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +24 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +27 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/SmsReadPluginPlugin/SmsReadPlugin.swift +6 -0
- package/ios/Sources/SmsReadPluginPlugin/SmsReadPluginPlugin.swift +25 -0
- package/ios/Tests/SmsReadPluginPluginTests/SmsReadPluginTests.swift +10 -0
- package/package.json +80 -0
package/Package.swift
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "SmsReadPluginAndroid",
|
|
6
|
+
platforms: [.iOS(.v15)],
|
|
7
|
+
products: [
|
|
8
|
+
.library(
|
|
9
|
+
name: "SmsReadPluginAndroid",
|
|
10
|
+
targets: ["SmsReadPluginPlugin"])
|
|
11
|
+
],
|
|
12
|
+
dependencies: [
|
|
13
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
|
14
|
+
],
|
|
15
|
+
targets: [
|
|
16
|
+
.target(
|
|
17
|
+
name: "SmsReadPluginPlugin",
|
|
18
|
+
dependencies: [
|
|
19
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
20
|
+
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
21
|
+
],
|
|
22
|
+
path: "ios/Sources/SmsReadPluginPlugin"),
|
|
23
|
+
.testTarget(
|
|
24
|
+
name: "SmsReadPluginPluginTests",
|
|
25
|
+
dependencies: ["SmsReadPluginPlugin"],
|
|
26
|
+
path: "ios/Tests/SmsReadPluginPluginTests")
|
|
27
|
+
]
|
|
28
|
+
)
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# sms-read-plugin-android
|
|
2
|
+
|
|
3
|
+
A plugin used to read sms for android apps
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
To use npm
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install sms-read-plugin-android
|
|
11
|
+
````
|
|
12
|
+
|
|
13
|
+
To use yarn
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yarn add sms-read-plugin-android
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Sync native files
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx cap sync
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
<docgen-index>
|
|
28
|
+
|
|
29
|
+
* [`startListening(...)`](#startlistening)
|
|
30
|
+
* [`stopListening()`](#stoplistening)
|
|
31
|
+
* [`addListener('smsReceived', ...)`](#addlistenersmsreceived-)
|
|
32
|
+
* [`addListener('smsError', ...)`](#addlistenersmserror-)
|
|
33
|
+
* [`removeAllListeners()`](#removealllisteners)
|
|
34
|
+
* [Interfaces](#interfaces)
|
|
35
|
+
* [Type Aliases](#type-aliases)
|
|
36
|
+
|
|
37
|
+
</docgen-index>
|
|
38
|
+
|
|
39
|
+
<docgen-api>
|
|
40
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
41
|
+
|
|
42
|
+
### startListening(...)
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
startListening(options?: StartListeningOptions | undefined) => Promise<SmsReceivedResult>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Start the SMS User Consent flow.
|
|
49
|
+
|
|
50
|
+
Registers with Google Play Services (`SmsRetrieverClient.startSmsUserConsent`)
|
|
51
|
+
and waits for a single incoming SMS. When a matching message arrives the
|
|
52
|
+
system shows a one-tap consent dialog; if the user allows it, the promise
|
|
53
|
+
resolves with the full message body and (best-effort) the extracted OTP.
|
|
54
|
+
|
|
55
|
+
This is a one-shot operation: it resolves/rejects exactly once and then
|
|
56
|
+
stops listening. Call it again to listen for the next message. Internally
|
|
57
|
+
it times out after ~5 minutes (enforced by Play Services).
|
|
58
|
+
|
|
59
|
+
Requires NO SMS permissions and NO app hash.
|
|
60
|
+
|
|
61
|
+
| Param | Type |
|
|
62
|
+
| ------------- | ----------------------------------------------------------------------- |
|
|
63
|
+
| **`options`** | <code><a href="#startlisteningoptions">StartListeningOptions</a></code> |
|
|
64
|
+
|
|
65
|
+
**Returns:** <code>Promise<<a href="#smsreceivedresult">SmsReceivedResult</a>></code>
|
|
66
|
+
|
|
67
|
+
--------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
### stopListening()
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
stopListening() => Promise<void>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Stop an in-progress `startListening` call early.
|
|
77
|
+
|
|
78
|
+
Unregisters the broadcast receiver and rejects any pending
|
|
79
|
+
`startListening` promise with code `CANCELLED`.
|
|
80
|
+
|
|
81
|
+
--------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
### addListener('smsReceived', ...)
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
addListener(eventName: 'smsReceived', listenerFunc: (event: SmsReceivedResult) => void) => Promise<PluginListenerHandle>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Listen for a received SMS. Fires in addition to the `startListening`
|
|
91
|
+
promise resolving, for consumers that prefer an event-based API.
|
|
92
|
+
|
|
93
|
+
| Param | Type |
|
|
94
|
+
| ------------------ | ----------------------------------------------------------------------------------- |
|
|
95
|
+
| **`eventName`** | <code>'smsReceived'</code> |
|
|
96
|
+
| **`listenerFunc`** | <code>(event: <a href="#smsreceivedresult">SmsReceivedResult</a>) => void</code> |
|
|
97
|
+
|
|
98
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
99
|
+
|
|
100
|
+
--------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
### addListener('smsError', ...)
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
addListener(eventName: 'smsError', listenerFunc: (event: SmsErrorEvent) => void) => Promise<PluginListenerHandle>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Listen for errors during the consent flow (timeout, denied consent, etc.).
|
|
110
|
+
|
|
111
|
+
| Param | Type |
|
|
112
|
+
| ------------------ | --------------------------------------------------------------------------- |
|
|
113
|
+
| **`eventName`** | <code>'smsError'</code> |
|
|
114
|
+
| **`listenerFunc`** | <code>(event: <a href="#smserrorevent">SmsErrorEvent</a>) => void</code> |
|
|
115
|
+
|
|
116
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
117
|
+
|
|
118
|
+
--------------------
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
### removeAllListeners()
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
removeAllListeners() => Promise<void>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Remove all registered listeners for this plugin.
|
|
128
|
+
|
|
129
|
+
--------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
### Interfaces
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
#### SmsReceivedResult
|
|
136
|
+
|
|
137
|
+
| Prop | Type | Description |
|
|
138
|
+
| ------------- | ------------------- | ---------------------------------------------------------------------- |
|
|
139
|
+
| **`message`** | <code>string</code> | The full text of the SMS the user consented to share. |
|
|
140
|
+
| **`otp`** | <code>string</code> | The OTP extracted from `message` using `otpPattern`, if one was found. |
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
#### StartListeningOptions
|
|
144
|
+
|
|
145
|
+
| Prop | Type | Description |
|
|
146
|
+
| ----------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
147
|
+
| **`senderPhoneNumber`** | <code>string</code> | If set, only messages originating from this phone number trigger the consent prompt. Leave undefined to accept a message from any sender. |
|
|
148
|
+
| **`otpPattern`** | <code>string</code> | Custom regular expression (as a string) used to extract the OTP from the message body. The first capturing group is preferred; otherwise the whole match is used. Defaults to `\d{4,8}` (a 4–8 digit run). |
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
#### PluginListenerHandle
|
|
152
|
+
|
|
153
|
+
| Prop | Type |
|
|
154
|
+
| ------------ | ----------------------------------------- |
|
|
155
|
+
| **`remove`** | <code>() => Promise<void></code> |
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
#### SmsErrorEvent
|
|
159
|
+
|
|
160
|
+
| Prop | Type | Description |
|
|
161
|
+
| ------------- | ----------------------------------------------------- | ---------------------------------------------- |
|
|
162
|
+
| **`code`** | <code><a href="#smserrorcode">SmsErrorCode</a></code> | Stable machine-readable error code. |
|
|
163
|
+
| **`message`** | <code>string</code> | Human-readable description of what went wrong. |
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
### Type Aliases
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
#### SmsErrorCode
|
|
170
|
+
|
|
171
|
+
<code>'TIMEOUT' | 'CONSENT_DENIED' | 'NO_MESSAGE' | 'CANCELLED' | 'API_ERROR'</code>
|
|
172
|
+
|
|
173
|
+
</docgen-api>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'SmsReadPluginAndroid'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.homepage = package['repository']['url']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
|
+
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
|
+
s.ios.deployment_target = '15.0'
|
|
15
|
+
s.dependency 'Capacitor'
|
|
16
|
+
s.swift_version = '5.1'
|
|
17
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
|
|
4
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
|
|
5
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
|
|
6
|
+
playServicesAuthApiPhoneVersion = project.hasProperty('playServicesAuthApiPhoneVersion') ? rootProject.ext.playServicesAuthApiPhoneVersion : '18.2.0'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
buildscript {
|
|
10
|
+
repositories {
|
|
11
|
+
google()
|
|
12
|
+
mavenCentral()
|
|
13
|
+
}
|
|
14
|
+
dependencies {
|
|
15
|
+
classpath 'com.android.tools.build:gradle:8.13.0'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
apply plugin: 'com.android.library'
|
|
20
|
+
|
|
21
|
+
android {
|
|
22
|
+
namespace = "com.gokulnaryanan.voltpath.sms"
|
|
23
|
+
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
24
|
+
defaultConfig {
|
|
25
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
|
|
26
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
|
|
27
|
+
versionCode 1
|
|
28
|
+
versionName "1.0"
|
|
29
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
30
|
+
}
|
|
31
|
+
buildTypes {
|
|
32
|
+
release {
|
|
33
|
+
minifyEnabled false
|
|
34
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
lintOptions {
|
|
38
|
+
abortOnError = false
|
|
39
|
+
}
|
|
40
|
+
compileOptions {
|
|
41
|
+
sourceCompatibility JavaVersion.VERSION_21
|
|
42
|
+
targetCompatibility JavaVersion.VERSION_21
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
repositories {
|
|
48
|
+
google()
|
|
49
|
+
mavenCentral()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
dependencies {
|
|
54
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
55
|
+
implementation project(':capacitor-android')
|
|
56
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
57
|
+
implementation "com.google.android.gms:play-services-auth-api-phone:$playServicesAuthApiPhoneVersion"
|
|
58
|
+
testImplementation "junit:junit:$junitVersion"
|
|
59
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
60
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
61
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package com.gokulnaryanan.voltpath.sms;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.Nullable;
|
|
4
|
+
import java.util.regex.Matcher;
|
|
5
|
+
import java.util.regex.Pattern;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pure, framework-free logic for the SMS Read plugin.
|
|
9
|
+
*
|
|
10
|
+
* Kept separate from {@link SmsReadPluginPlugin} (the Capacitor bridge) so it
|
|
11
|
+
* can be unit-tested without an Android runtime or the Play Services SDK.
|
|
12
|
+
*/
|
|
13
|
+
public class SmsReadPlugin {
|
|
14
|
+
|
|
15
|
+
/** Matches a run of 4 to 8 digits, the typical shape of an OTP. */
|
|
16
|
+
static final String DEFAULT_OTP_PATTERN = "\\d{4,8}";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract an OTP from an SMS body.
|
|
20
|
+
*
|
|
21
|
+
* @param message the full SMS text
|
|
22
|
+
* @param pattern a regular expression; when it contains a capturing group,
|
|
23
|
+
* group 1 is returned, otherwise the whole match is. Falls
|
|
24
|
+
* back to {@link #DEFAULT_OTP_PATTERN} when null/blank.
|
|
25
|
+
* @return the extracted OTP, or {@code null} if nothing matched or the
|
|
26
|
+
* pattern was invalid.
|
|
27
|
+
*/
|
|
28
|
+
@Nullable
|
|
29
|
+
public String extractOtp(@Nullable String message, @Nullable String pattern) {
|
|
30
|
+
if (message == null || message.isEmpty()) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
String effectivePattern = (pattern == null || pattern.trim().isEmpty()) ? DEFAULT_OTP_PATTERN : pattern;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
Matcher matcher = Pattern.compile(effectivePattern).matcher(message);
|
|
38
|
+
if (!matcher.find()) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (matcher.groupCount() >= 1 && matcher.group(1) != null) {
|
|
42
|
+
return matcher.group(1);
|
|
43
|
+
}
|
|
44
|
+
return matcher.group();
|
|
45
|
+
} catch (RuntimeException ex) {
|
|
46
|
+
// Invalid regex supplied by the caller — treat as "no OTP found".
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
package com.gokulnaryanan.voltpath.sms;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.content.BroadcastReceiver;
|
|
5
|
+
import android.content.Context;
|
|
6
|
+
import android.content.Intent;
|
|
7
|
+
import android.content.IntentFilter;
|
|
8
|
+
import android.os.Bundle;
|
|
9
|
+
import androidx.activity.result.ActivityResult;
|
|
10
|
+
import androidx.core.content.ContextCompat;
|
|
11
|
+
import com.getcapacitor.JSObject;
|
|
12
|
+
import com.getcapacitor.Logger;
|
|
13
|
+
import com.getcapacitor.Plugin;
|
|
14
|
+
import com.getcapacitor.PluginCall;
|
|
15
|
+
import com.getcapacitor.PluginMethod;
|
|
16
|
+
import com.getcapacitor.annotation.ActivityCallback;
|
|
17
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
18
|
+
import com.google.android.gms.auth.api.phone.SmsRetriever;
|
|
19
|
+
import com.google.android.gms.auth.api.phone.SmsRetrieverClient;
|
|
20
|
+
import com.google.android.gms.common.api.CommonStatusCodes;
|
|
21
|
+
import com.google.android.gms.common.api.Status;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Capacitor bridge over the Google Play Services SMS User Consent API.
|
|
25
|
+
*
|
|
26
|
+
* Flow:
|
|
27
|
+
* 1. JS calls {@code startListening()} -> we call
|
|
28
|
+
* {@link SmsRetrieverClient#startSmsUserConsent(String)} and register a
|
|
29
|
+
* broadcast receiver for {@link SmsRetriever#SMS_RETRIEVED_ACTION}.
|
|
30
|
+
* 2. When a matching SMS arrives, Play Services broadcasts a consent intent.
|
|
31
|
+
* We launch it with {@code startActivityForResult}, which shows the
|
|
32
|
+
* one-tap consent dialog.
|
|
33
|
+
* 3. On "Allow", the result carries the full message body. We extract the
|
|
34
|
+
* OTP and resolve the pending call (and emit a {@code smsReceived} event).
|
|
35
|
+
*
|
|
36
|
+
* Requires NO SMS permissions and NO app hash — that is the whole point of the
|
|
37
|
+
* User Consent variant.
|
|
38
|
+
*/
|
|
39
|
+
@CapacitorPlugin(name = "SmsReadPlugin")
|
|
40
|
+
public class SmsReadPluginPlugin extends Plugin {
|
|
41
|
+
|
|
42
|
+
private static final String TAG = "SmsReadPlugin";
|
|
43
|
+
|
|
44
|
+
private final SmsReadPlugin implementation = new SmsReadPlugin();
|
|
45
|
+
|
|
46
|
+
/** The in-flight {@code startListening} call, kept alive until a result. */
|
|
47
|
+
private PluginCall pendingCall;
|
|
48
|
+
|
|
49
|
+
/** Regex used to extract the OTP from the consented message. */
|
|
50
|
+
private String otpPattern;
|
|
51
|
+
|
|
52
|
+
/** Receiver for {@link SmsRetriever#SMS_RETRIEVED_ACTION}; null when idle. */
|
|
53
|
+
private BroadcastReceiver smsReceiver;
|
|
54
|
+
|
|
55
|
+
@PluginMethod
|
|
56
|
+
public void startListening(PluginCall call) {
|
|
57
|
+
// Only one listen at a time — cancel any previous attempt cleanly.
|
|
58
|
+
cancelPending("Superseded by a new startListening call");
|
|
59
|
+
teardownReceiver();
|
|
60
|
+
|
|
61
|
+
otpPattern = call.getString("otpPattern");
|
|
62
|
+
String senderPhoneNumber = call.getString("senderPhoneNumber");
|
|
63
|
+
|
|
64
|
+
Context context = getContext();
|
|
65
|
+
SmsRetrieverClient client = SmsRetriever.getClient(context);
|
|
66
|
+
|
|
67
|
+
registerReceiver(context);
|
|
68
|
+
|
|
69
|
+
// null sender => accept a message from any number.
|
|
70
|
+
client
|
|
71
|
+
.startSmsUserConsent(senderPhoneNumber)
|
|
72
|
+
.addOnSuccessListener((unused) -> {
|
|
73
|
+
Logger.debug(TAG, "SMS User Consent listener started");
|
|
74
|
+
call.setKeepAlive(true);
|
|
75
|
+
pendingCall = call;
|
|
76
|
+
})
|
|
77
|
+
.addOnFailureListener((error) -> {
|
|
78
|
+
Logger.error(TAG, "Failed to start SMS User Consent", error);
|
|
79
|
+
teardownReceiver();
|
|
80
|
+
call.reject("Failed to start SMS User Consent: " + error.getMessage(), "API_ERROR");
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@PluginMethod
|
|
85
|
+
public void stopListening(PluginCall call) {
|
|
86
|
+
teardownReceiver();
|
|
87
|
+
cancelPending("Listening cancelled by stopListening");
|
|
88
|
+
call.resolve();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Override
|
|
92
|
+
protected void handleOnDestroy() {
|
|
93
|
+
teardownReceiver();
|
|
94
|
+
cancelPending("Plugin destroyed");
|
|
95
|
+
super.handleOnDestroy();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private void registerReceiver(Context context) {
|
|
99
|
+
smsReceiver = new BroadcastReceiver() {
|
|
100
|
+
@Override
|
|
101
|
+
public void onReceive(Context ctx, Intent intent) {
|
|
102
|
+
if (!SmsRetriever.SMS_RETRIEVED_ACTION.equals(intent.getAction())) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
Bundle extras = intent.getExtras();
|
|
106
|
+
if (extras == null) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
Status status = (Status) extras.get(SmsRetriever.EXTRA_STATUS);
|
|
110
|
+
if (status == null) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
switch (status.getStatusCode()) {
|
|
114
|
+
case CommonStatusCodes.SUCCESS:
|
|
115
|
+
Intent consentIntent = extras.getParcelable(SmsRetriever.EXTRA_CONSENT_INTENT);
|
|
116
|
+
launchConsent(consentIntent);
|
|
117
|
+
break;
|
|
118
|
+
case CommonStatusCodes.TIMEOUT:
|
|
119
|
+
teardownReceiver();
|
|
120
|
+
emitError("TIMEOUT", "Timed out waiting for an SMS (5 minute limit)");
|
|
121
|
+
rejectPending("Timed out waiting for an SMS", "TIMEOUT");
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
// Other status codes are not expected for this action.
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
IntentFilter filter = new IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION);
|
|
131
|
+
// The broadcast originates from Play Services (an external process), so
|
|
132
|
+
// the receiver must be exported; it is gated by SEND_PERMISSION, which
|
|
133
|
+
// only Play Services holds. ContextCompat handles the Android 13+ flag.
|
|
134
|
+
ContextCompat.registerReceiver(context, smsReceiver, filter, SmsRetriever.SEND_PERMISSION, null, ContextCompat.RECEIVER_EXPORTED);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private void launchConsent(Intent consentIntent) {
|
|
138
|
+
if (consentIntent == null) {
|
|
139
|
+
teardownReceiver();
|
|
140
|
+
emitError("NO_MESSAGE", "Consent intent was missing from the broadcast");
|
|
141
|
+
rejectPending("Consent intent was missing", "NO_MESSAGE");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (pendingCall == null) {
|
|
145
|
+
// Nothing is awaiting a result (e.g. already cancelled).
|
|
146
|
+
teardownReceiver();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Hands the consent dialog to Capacitor; the result returns to
|
|
150
|
+
// handleConsentResult below, tied to pendingCall.
|
|
151
|
+
startActivityForResult(pendingCall, consentIntent, "handleConsentResult");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@ActivityCallback
|
|
155
|
+
private void handleConsentResult(PluginCall call, ActivityResult result) {
|
|
156
|
+
teardownReceiver();
|
|
157
|
+
|
|
158
|
+
if (call == null) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) {
|
|
162
|
+
emitError("CONSENT_DENIED", "User denied SMS consent");
|
|
163
|
+
call.reject("User denied SMS consent", "CONSENT_DENIED");
|
|
164
|
+
clearPending(call);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
String message = result.getData().getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE);
|
|
169
|
+
if (message == null) {
|
|
170
|
+
emitError("NO_MESSAGE", "Consent granted but no message body was returned");
|
|
171
|
+
call.reject("No message body returned", "NO_MESSAGE");
|
|
172
|
+
clearPending(call);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
String otp = implementation.extractOtp(message, otpPattern);
|
|
177
|
+
|
|
178
|
+
JSObject payload = new JSObject();
|
|
179
|
+
payload.put("message", message);
|
|
180
|
+
if (otp != null) {
|
|
181
|
+
payload.put("otp", otp);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
notifyListeners("smsReceived", payload);
|
|
185
|
+
call.resolve(payload);
|
|
186
|
+
clearPending(call);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private void teardownReceiver() {
|
|
190
|
+
if (smsReceiver != null) {
|
|
191
|
+
try {
|
|
192
|
+
getContext().unregisterReceiver(smsReceiver);
|
|
193
|
+
} catch (IllegalArgumentException ignored) {
|
|
194
|
+
// Already unregistered — safe to ignore.
|
|
195
|
+
}
|
|
196
|
+
smsReceiver = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Reject + release the pending call (used for cancellations). */
|
|
201
|
+
private void cancelPending(String reason) {
|
|
202
|
+
rejectPending(reason, "CANCELLED");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private void rejectPending(String reason, String code) {
|
|
206
|
+
if (pendingCall != null) {
|
|
207
|
+
pendingCall.reject(reason, code);
|
|
208
|
+
pendingCall.setKeepAlive(false);
|
|
209
|
+
pendingCall = null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Release the pending reference after a terminal resolve/reject. */
|
|
214
|
+
private void clearPending(PluginCall call) {
|
|
215
|
+
call.setKeepAlive(false);
|
|
216
|
+
if (pendingCall == call) {
|
|
217
|
+
pendingCall = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private void emitError(String code, String message) {
|
|
222
|
+
JSObject payload = new JSObject();
|
|
223
|
+
payload.put("code", code);
|
|
224
|
+
payload.put("message", message);
|
|
225
|
+
notifyListeners("smsError", payload);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
File without changes
|
package/dist/docs.json
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
{
|
|
2
|
+
"api": {
|
|
3
|
+
"name": "SmsReadPluginPlugin",
|
|
4
|
+
"slug": "smsreadpluginplugin",
|
|
5
|
+
"docs": "",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"methods": [
|
|
8
|
+
{
|
|
9
|
+
"name": "startListening",
|
|
10
|
+
"signature": "(options?: StartListeningOptions | undefined) => Promise<SmsReceivedResult>",
|
|
11
|
+
"parameters": [
|
|
12
|
+
{
|
|
13
|
+
"name": "options",
|
|
14
|
+
"docs": "",
|
|
15
|
+
"type": "StartListeningOptions | undefined"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"returns": "Promise<SmsReceivedResult>",
|
|
19
|
+
"tags": [
|
|
20
|
+
{
|
|
21
|
+
"name": "platform",
|
|
22
|
+
"text": "android"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"docs": "Start the SMS User Consent flow.\n\nRegisters with Google Play Services (`SmsRetrieverClient.startSmsUserConsent`)\nand waits for a single incoming SMS. When a matching message arrives the\nsystem shows a one-tap consent dialog; if the user allows it, the promise\nresolves with the full message body and (best-effort) the extracted OTP.\n\nThis is a one-shot operation: it resolves/rejects exactly once and then\nstops listening. Call it again to listen for the next message. Internally\nit times out after ~5 minutes (enforced by Play Services).\n\nRequires NO SMS permissions and NO app hash.",
|
|
26
|
+
"complexTypes": [
|
|
27
|
+
"SmsReceivedResult",
|
|
28
|
+
"StartListeningOptions"
|
|
29
|
+
],
|
|
30
|
+
"slug": "startlistening"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "stopListening",
|
|
34
|
+
"signature": "() => Promise<void>",
|
|
35
|
+
"parameters": [],
|
|
36
|
+
"returns": "Promise<void>",
|
|
37
|
+
"tags": [
|
|
38
|
+
{
|
|
39
|
+
"name": "platform",
|
|
40
|
+
"text": "android"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"docs": "Stop an in-progress `startListening` call early.\n\nUnregisters the broadcast receiver and rejects any pending\n`startListening` promise with code `CANCELLED`.",
|
|
44
|
+
"complexTypes": [],
|
|
45
|
+
"slug": "stoplistening"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "addListener",
|
|
49
|
+
"signature": "(eventName: 'smsReceived', listenerFunc: (event: SmsReceivedResult) => void) => Promise<PluginListenerHandle>",
|
|
50
|
+
"parameters": [
|
|
51
|
+
{
|
|
52
|
+
"name": "eventName",
|
|
53
|
+
"docs": "",
|
|
54
|
+
"type": "'smsReceived'"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "listenerFunc",
|
|
58
|
+
"docs": "",
|
|
59
|
+
"type": "(event: SmsReceivedResult) => void"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
63
|
+
"tags": [
|
|
64
|
+
{
|
|
65
|
+
"name": "platform",
|
|
66
|
+
"text": "android"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"docs": "Listen for a received SMS. Fires in addition to the `startListening`\npromise resolving, for consumers that prefer an event-based API.",
|
|
70
|
+
"complexTypes": [
|
|
71
|
+
"PluginListenerHandle",
|
|
72
|
+
"SmsReceivedResult"
|
|
73
|
+
],
|
|
74
|
+
"slug": "addlistenersmsreceived-"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "addListener",
|
|
78
|
+
"signature": "(eventName: 'smsError', listenerFunc: (event: SmsErrorEvent) => void) => Promise<PluginListenerHandle>",
|
|
79
|
+
"parameters": [
|
|
80
|
+
{
|
|
81
|
+
"name": "eventName",
|
|
82
|
+
"docs": "",
|
|
83
|
+
"type": "'smsError'"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "listenerFunc",
|
|
87
|
+
"docs": "",
|
|
88
|
+
"type": "(event: SmsErrorEvent) => void"
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
92
|
+
"tags": [
|
|
93
|
+
{
|
|
94
|
+
"name": "platform",
|
|
95
|
+
"text": "android"
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
"docs": "Listen for errors during the consent flow (timeout, denied consent, etc.).",
|
|
99
|
+
"complexTypes": [
|
|
100
|
+
"PluginListenerHandle",
|
|
101
|
+
"SmsErrorEvent"
|
|
102
|
+
],
|
|
103
|
+
"slug": "addlistenersmserror-"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "removeAllListeners",
|
|
107
|
+
"signature": "() => Promise<void>",
|
|
108
|
+
"parameters": [],
|
|
109
|
+
"returns": "Promise<void>",
|
|
110
|
+
"tags": [],
|
|
111
|
+
"docs": "Remove all registered listeners for this plugin.",
|
|
112
|
+
"complexTypes": [],
|
|
113
|
+
"slug": "removealllisteners"
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"properties": []
|
|
117
|
+
},
|
|
118
|
+
"interfaces": [
|
|
119
|
+
{
|
|
120
|
+
"name": "SmsReceivedResult",
|
|
121
|
+
"slug": "smsreceivedresult",
|
|
122
|
+
"docs": "",
|
|
123
|
+
"tags": [],
|
|
124
|
+
"methods": [],
|
|
125
|
+
"properties": [
|
|
126
|
+
{
|
|
127
|
+
"name": "message",
|
|
128
|
+
"tags": [],
|
|
129
|
+
"docs": "The full text of the SMS the user consented to share.",
|
|
130
|
+
"complexTypes": [],
|
|
131
|
+
"type": "string"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "otp",
|
|
135
|
+
"tags": [],
|
|
136
|
+
"docs": "The OTP extracted from `message` using `otpPattern`, if one was found.",
|
|
137
|
+
"complexTypes": [],
|
|
138
|
+
"type": "string | undefined"
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "StartListeningOptions",
|
|
144
|
+
"slug": "startlisteningoptions",
|
|
145
|
+
"docs": "",
|
|
146
|
+
"tags": [],
|
|
147
|
+
"methods": [],
|
|
148
|
+
"properties": [
|
|
149
|
+
{
|
|
150
|
+
"name": "senderPhoneNumber",
|
|
151
|
+
"tags": [],
|
|
152
|
+
"docs": "If set, only messages originating from this phone number trigger the\nconsent prompt. Leave undefined to accept a message from any sender.",
|
|
153
|
+
"complexTypes": [],
|
|
154
|
+
"type": "string | undefined"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "otpPattern",
|
|
158
|
+
"tags": [],
|
|
159
|
+
"docs": "Custom regular expression (as a string) used to extract the OTP from the\nmessage body. The first capturing group is preferred; otherwise the whole\nmatch is used. Defaults to `\\d{4,8}` (a 4–8 digit run).",
|
|
160
|
+
"complexTypes": [],
|
|
161
|
+
"type": "string | undefined"
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "PluginListenerHandle",
|
|
167
|
+
"slug": "pluginlistenerhandle",
|
|
168
|
+
"docs": "",
|
|
169
|
+
"tags": [],
|
|
170
|
+
"methods": [],
|
|
171
|
+
"properties": [
|
|
172
|
+
{
|
|
173
|
+
"name": "remove",
|
|
174
|
+
"tags": [],
|
|
175
|
+
"docs": "",
|
|
176
|
+
"complexTypes": [],
|
|
177
|
+
"type": "() => Promise<void>"
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"name": "SmsErrorEvent",
|
|
183
|
+
"slug": "smserrorevent",
|
|
184
|
+
"docs": "",
|
|
185
|
+
"tags": [],
|
|
186
|
+
"methods": [],
|
|
187
|
+
"properties": [
|
|
188
|
+
{
|
|
189
|
+
"name": "code",
|
|
190
|
+
"tags": [],
|
|
191
|
+
"docs": "Stable machine-readable error code.",
|
|
192
|
+
"complexTypes": [
|
|
193
|
+
"SmsErrorCode"
|
|
194
|
+
],
|
|
195
|
+
"type": "SmsErrorCode"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "message",
|
|
199
|
+
"tags": [],
|
|
200
|
+
"docs": "Human-readable description of what went wrong.",
|
|
201
|
+
"complexTypes": [],
|
|
202
|
+
"type": "string"
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"enums": [],
|
|
208
|
+
"typeAliases": [
|
|
209
|
+
{
|
|
210
|
+
"name": "SmsErrorCode",
|
|
211
|
+
"slug": "smserrorcode",
|
|
212
|
+
"docs": "",
|
|
213
|
+
"types": [
|
|
214
|
+
{
|
|
215
|
+
"text": "'TIMEOUT'",
|
|
216
|
+
"complexTypes": []
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"text": "'CONSENT_DENIED'",
|
|
220
|
+
"complexTypes": []
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"text": "'NO_MESSAGE'",
|
|
224
|
+
"complexTypes": []
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"text": "'CANCELLED'",
|
|
228
|
+
"complexTypes": []
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"text": "'API_ERROR'",
|
|
232
|
+
"complexTypes": []
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
],
|
|
237
|
+
"pluginConfigs": []
|
|
238
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { PluginListenerHandle } from '@capacitor/core';
|
|
2
|
+
export interface SmsReadPluginPlugin {
|
|
3
|
+
/**
|
|
4
|
+
* Start the SMS User Consent flow.
|
|
5
|
+
*
|
|
6
|
+
* Registers with Google Play Services (`SmsRetrieverClient.startSmsUserConsent`)
|
|
7
|
+
* and waits for a single incoming SMS. When a matching message arrives the
|
|
8
|
+
* system shows a one-tap consent dialog; if the user allows it, the promise
|
|
9
|
+
* resolves with the full message body and (best-effort) the extracted OTP.
|
|
10
|
+
*
|
|
11
|
+
* This is a one-shot operation: it resolves/rejects exactly once and then
|
|
12
|
+
* stops listening. Call it again to listen for the next message. Internally
|
|
13
|
+
* it times out after ~5 minutes (enforced by Play Services).
|
|
14
|
+
*
|
|
15
|
+
* Requires NO SMS permissions and NO app hash.
|
|
16
|
+
*
|
|
17
|
+
* @platform android
|
|
18
|
+
*/
|
|
19
|
+
startListening(options?: StartListeningOptions): Promise<SmsReceivedResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Stop an in-progress `startListening` call early.
|
|
22
|
+
*
|
|
23
|
+
* Unregisters the broadcast receiver and rejects any pending
|
|
24
|
+
* `startListening` promise with code `CANCELLED`.
|
|
25
|
+
*
|
|
26
|
+
* @platform android
|
|
27
|
+
*/
|
|
28
|
+
stopListening(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Listen for a received SMS. Fires in addition to the `startListening`
|
|
31
|
+
* promise resolving, for consumers that prefer an event-based API.
|
|
32
|
+
*
|
|
33
|
+
* @platform android
|
|
34
|
+
*/
|
|
35
|
+
addListener(eventName: 'smsReceived', listenerFunc: (event: SmsReceivedResult) => void): Promise<PluginListenerHandle>;
|
|
36
|
+
/**
|
|
37
|
+
* Listen for errors during the consent flow (timeout, denied consent, etc.).
|
|
38
|
+
*
|
|
39
|
+
* @platform android
|
|
40
|
+
*/
|
|
41
|
+
addListener(eventName: 'smsError', listenerFunc: (event: SmsErrorEvent) => void): Promise<PluginListenerHandle>;
|
|
42
|
+
/**
|
|
43
|
+
* Remove all registered listeners for this plugin.
|
|
44
|
+
*/
|
|
45
|
+
removeAllListeners(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export interface StartListeningOptions {
|
|
48
|
+
/**
|
|
49
|
+
* If set, only messages originating from this phone number trigger the
|
|
50
|
+
* consent prompt. Leave undefined to accept a message from any sender.
|
|
51
|
+
*/
|
|
52
|
+
senderPhoneNumber?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Custom regular expression (as a string) used to extract the OTP from the
|
|
55
|
+
* message body. The first capturing group is preferred; otherwise the whole
|
|
56
|
+
* match is used. Defaults to `\d{4,8}` (a 4–8 digit run).
|
|
57
|
+
*/
|
|
58
|
+
otpPattern?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface SmsReceivedResult {
|
|
61
|
+
/**
|
|
62
|
+
* The full text of the SMS the user consented to share.
|
|
63
|
+
*/
|
|
64
|
+
message: string;
|
|
65
|
+
/**
|
|
66
|
+
* The OTP extracted from `message` using `otpPattern`, if one was found.
|
|
67
|
+
*/
|
|
68
|
+
otp?: string;
|
|
69
|
+
}
|
|
70
|
+
export type SmsErrorCode = 'TIMEOUT' | 'CONSENT_DENIED' | 'NO_MESSAGE' | 'CANCELLED' | 'API_ERROR';
|
|
71
|
+
export interface SmsErrorEvent {
|
|
72
|
+
/**
|
|
73
|
+
* Stable machine-readable error code.
|
|
74
|
+
*/
|
|
75
|
+
code: SmsErrorCode;
|
|
76
|
+
/**
|
|
77
|
+
* Human-readable description of what went wrong.
|
|
78
|
+
*/
|
|
79
|
+
message: string;
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface SmsReadPluginPlugin {\n /**\n * Start the SMS User Consent flow.\n *\n * Registers with Google Play Services (`SmsRetrieverClient.startSmsUserConsent`)\n * and waits for a single incoming SMS. When a matching message arrives the\n * system shows a one-tap consent dialog; if the user allows it, the promise\n * resolves with the full message body and (best-effort) the extracted OTP.\n *\n * This is a one-shot operation: it resolves/rejects exactly once and then\n * stops listening. Call it again to listen for the next message. Internally\n * it times out after ~5 minutes (enforced by Play Services).\n *\n * Requires NO SMS permissions and NO app hash.\n *\n * @platform android\n */\n startListening(options?: StartListeningOptions): Promise<SmsReceivedResult>;\n\n /**\n * Stop an in-progress `startListening` call early.\n *\n * Unregisters the broadcast receiver and rejects any pending\n * `startListening` promise with code `CANCELLED`.\n *\n * @platform android\n */\n stopListening(): Promise<void>;\n\n /**\n * Listen for a received SMS. Fires in addition to the `startListening`\n * promise resolving, for consumers that prefer an event-based API.\n *\n * @platform android\n */\n addListener(\n eventName: 'smsReceived',\n listenerFunc: (event: SmsReceivedResult) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for errors during the consent flow (timeout, denied consent, etc.).\n *\n * @platform android\n */\n addListener(eventName: 'smsError', listenerFunc: (event: SmsErrorEvent) => void): Promise<PluginListenerHandle>;\n\n /**\n * Remove all registered listeners for this plugin.\n */\n removeAllListeners(): Promise<void>;\n}\n\nexport interface StartListeningOptions {\n /**\n * If set, only messages originating from this phone number trigger the\n * consent prompt. Leave undefined to accept a message from any sender.\n */\n senderPhoneNumber?: string;\n\n /**\n * Custom regular expression (as a string) used to extract the OTP from the\n * message body. The first capturing group is preferred; otherwise the whole\n * match is used. Defaults to `\\d{4,8}` (a 4–8 digit run).\n */\n otpPattern?: string;\n}\n\nexport interface SmsReceivedResult {\n /**\n * The full text of the SMS the user consented to share.\n */\n message: string;\n\n /**\n * The OTP extracted from `message` using `otpPattern`, if one was found.\n */\n otp?: string;\n}\n\nexport type SmsErrorCode = 'TIMEOUT' | 'CONSENT_DENIED' | 'NO_MESSAGE' | 'CANCELLED' | 'API_ERROR';\n\nexport interface SmsErrorEvent {\n /**\n * Stable machine-readable error code.\n */\n code: SmsErrorCode;\n\n /**\n * Human-readable description of what went wrong.\n */\n message: string;\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { registerPlugin } from '@capacitor/core';
|
|
2
|
+
const SmsReadPlugin = registerPlugin('SmsReadPlugin', {
|
|
3
|
+
web: () => import('./web').then((m) => new m.SmsReadPluginWeb()),
|
|
4
|
+
});
|
|
5
|
+
export * from './definitions';
|
|
6
|
+
export { SmsReadPlugin };
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,aAAa,GAAG,cAAc,CAAsB,eAAe,EAAE;IACzE,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,gBAAgB,EAAE,CAAC;CACjE,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\n\nimport type { SmsReadPluginPlugin } from './definitions';\n\nconst SmsReadPlugin = registerPlugin<SmsReadPluginPlugin>('SmsReadPlugin', {\n web: () => import('./web').then((m) => new m.SmsReadPluginWeb()),\n});\n\nexport * from './definitions';\nexport { SmsReadPlugin };\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { WebPlugin } from '@capacitor/core';
|
|
2
|
+
import type { SmsReadPluginPlugin, StartListeningOptions, SmsReceivedResult } from './definitions';
|
|
3
|
+
export declare class SmsReadPluginWeb extends WebPlugin implements SmsReadPluginPlugin {
|
|
4
|
+
startListening(_options?: StartListeningOptions): Promise<SmsReceivedResult>;
|
|
5
|
+
stopListening(): Promise<void>;
|
|
6
|
+
}
|
package/dist/esm/web.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { WebPlugin } from '@capacitor/core';
|
|
2
|
+
export class SmsReadPluginWeb extends WebPlugin {
|
|
3
|
+
async startListening(_options) {
|
|
4
|
+
throw this.unavailable('SMS User Consent is only available on Android.');
|
|
5
|
+
}
|
|
6
|
+
async stopListening() {
|
|
7
|
+
throw this.unavailable('SMS User Consent is only available on Android.');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,MAAM,OAAO,gBAAiB,SAAQ,SAAS;IAC7C,KAAK,CAAC,cAAc,CAAC,QAAgC;QACnD,MAAM,IAAI,CAAC,WAAW,CAAC,gDAAgD,CAAC,CAAC;IAC3E,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,MAAM,IAAI,CAAC,WAAW,CAAC,gDAAgD,CAAC,CAAC;IAC3E,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { SmsReadPluginPlugin, StartListeningOptions, SmsReceivedResult } from './definitions';\n\nexport class SmsReadPluginWeb extends WebPlugin implements SmsReadPluginPlugin {\n async startListening(_options?: StartListeningOptions): Promise<SmsReceivedResult> {\n throw this.unavailable('SMS User Consent is only available on Android.');\n }\n\n async stopListening(): Promise<void> {\n throw this.unavailable('SMS User Consent is only available on Android.');\n }\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@capacitor/core');
|
|
4
|
+
|
|
5
|
+
const SmsReadPlugin = core.registerPlugin('SmsReadPlugin', {
|
|
6
|
+
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.SmsReadPluginWeb()),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
class SmsReadPluginWeb extends core.WebPlugin {
|
|
10
|
+
async startListening(_options) {
|
|
11
|
+
throw this.unavailable('SMS User Consent is only available on Android.');
|
|
12
|
+
}
|
|
13
|
+
async stopListening() {
|
|
14
|
+
throw this.unavailable('SMS User Consent is only available on Android.');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
19
|
+
__proto__: null,
|
|
20
|
+
SmsReadPluginWeb: SmsReadPluginWeb
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
exports.SmsReadPlugin = SmsReadPlugin;
|
|
24
|
+
//# sourceMappingURL=plugin.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SmsReadPlugin = registerPlugin('SmsReadPlugin', {\n web: () => import('./web').then((m) => new m.SmsReadPluginWeb()),\n});\nexport * from './definitions';\nexport { SmsReadPlugin };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SmsReadPluginWeb extends WebPlugin {\n async startListening(_options) {\n throw this.unavailable('SMS User Consent is only available on Android.');\n }\n async stopListening() {\n throw this.unavailable('SMS User Consent is only available on Android.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,aAAa,GAAGA,mBAAc,CAAC,eAAe,EAAE;AACtD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,gBAAgB,EAAE,CAAC;AACpE,CAAC;;ACFM,MAAM,gBAAgB,SAASC,cAAS,CAAC;AAChD,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;AACnC,QAAQ,MAAM,IAAI,CAAC,WAAW,CAAC,gDAAgD,CAAC;AAChF,IAAI;AACJ,IAAI,MAAM,aAAa,GAAG;AAC1B,QAAQ,MAAM,IAAI,CAAC,WAAW,CAAC,gDAAgD,CAAC;AAChF,IAAI;AACJ;;;;;;;;;"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
var capacitorSmsReadPlugin = (function (exports, core) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const SmsReadPlugin = core.registerPlugin('SmsReadPlugin', {
|
|
5
|
+
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.SmsReadPluginWeb()),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
class SmsReadPluginWeb extends core.WebPlugin {
|
|
9
|
+
async startListening(_options) {
|
|
10
|
+
throw this.unavailable('SMS User Consent is only available on Android.');
|
|
11
|
+
}
|
|
12
|
+
async stopListening() {
|
|
13
|
+
throw this.unavailable('SMS User Consent is only available on Android.');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
18
|
+
__proto__: null,
|
|
19
|
+
SmsReadPluginWeb: SmsReadPluginWeb
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
exports.SmsReadPlugin = SmsReadPlugin;
|
|
23
|
+
|
|
24
|
+
return exports;
|
|
25
|
+
|
|
26
|
+
})({}, capacitorExports);
|
|
27
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SmsReadPlugin = registerPlugin('SmsReadPlugin', {\n web: () => import('./web').then((m) => new m.SmsReadPluginWeb()),\n});\nexport * from './definitions';\nexport { SmsReadPlugin };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SmsReadPluginWeb extends WebPlugin {\n async startListening(_options) {\n throw this.unavailable('SMS User Consent is only available on Android.');\n }\n async stopListening() {\n throw this.unavailable('SMS User Consent is only available on Android.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,aAAa,GAAGA,mBAAc,CAAC,eAAe,EAAE;IACtD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,gBAAgB,EAAE,CAAC;IACpE,CAAC;;ICFM,MAAM,gBAAgB,SAASC,cAAS,CAAC;IAChD,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;IACnC,QAAQ,MAAM,IAAI,CAAC,WAAW,CAAC,gDAAgD,CAAC;IAChF,IAAI;IACJ,IAAI,MAAM,aAAa,GAAG;IAC1B,QAAQ,MAAM,IAAI,CAAC,WAAW,CAAC,gDAAgD,CAAC;IAChF,IAAI;IACJ;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SMS reading via the User Consent API is an Android-only capability. On iOS,
|
|
6
|
+
* SMS OTPs are surfaced by the system keyboard (AutoFill) and there is no
|
|
7
|
+
* equivalent programmatic API, so these methods report as unavailable.
|
|
8
|
+
*/
|
|
9
|
+
@objc(SmsReadPluginPlugin)
|
|
10
|
+
public class SmsReadPluginPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
|
+
public let identifier = "SmsReadPluginPlugin"
|
|
12
|
+
public let jsName = "SmsReadPlugin"
|
|
13
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
14
|
+
CAPPluginMethod(name: "startListening", returnType: CAPPluginReturnPromise),
|
|
15
|
+
CAPPluginMethod(name: "stopListening", returnType: CAPPluginReturnPromise)
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
@objc func startListening(_ call: CAPPluginCall) {
|
|
19
|
+
call.unavailable("SMS User Consent is only available on Android.")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@objc func stopListening(_ call: CAPPluginCall) {
|
|
23
|
+
call.unavailable("SMS User Consent is only available on Android.")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import SmsReadPluginPlugin
|
|
3
|
+
|
|
4
|
+
class SmsReadPluginTests: XCTestCase {
|
|
5
|
+
func testPlaceholder() {
|
|
6
|
+
// SMS reading is Android-only; the iOS implementation is a no-op
|
|
7
|
+
// placeholder, so there is nothing functional to assert here.
|
|
8
|
+
XCTAssertNotNil(SmsReadPlugin())
|
|
9
|
+
}
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sms-read-plugin-android",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A plugin used to read sms for android apps",
|
|
5
|
+
"main": "dist/plugin.cjs.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
8
|
+
"unpkg": "dist/plugin.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"android/src/main/",
|
|
11
|
+
"android/build.gradle",
|
|
12
|
+
"dist/",
|
|
13
|
+
"ios/Sources",
|
|
14
|
+
"ios/Tests",
|
|
15
|
+
"Package.swift",
|
|
16
|
+
"SmsReadPluginAndroid.podspec"
|
|
17
|
+
],
|
|
18
|
+
"author": "Gokul Narayanan <ngokulnarayanan@gmail.com>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ev-voltpath/sms-read-plugin-capcitor-android.git.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/ev-voltpath/sms-read-plugin-capcitor-android.git/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"capacitor",
|
|
29
|
+
"plugin",
|
|
30
|
+
"native"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
34
|
+
"verify:ios": "xcodebuild -scheme SmsReadPluginAndroid -destination generic/platform=iOS",
|
|
35
|
+
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
36
|
+
"verify:web": "npm run build",
|
|
37
|
+
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
|
38
|
+
"fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
|
|
39
|
+
"eslint": "eslint . --ext ts",
|
|
40
|
+
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
41
|
+
"swiftlint": "node-swiftlint",
|
|
42
|
+
"docgen": "docgen --api SmsReadPluginPlugin --output-readme README.md --output-json dist/docs.json",
|
|
43
|
+
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
|
|
44
|
+
"clean": "rimraf ./dist",
|
|
45
|
+
"watch": "tsc --watch",
|
|
46
|
+
"prepublishOnly": "npm run build"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@capacitor/android": "^8.0.0",
|
|
50
|
+
"@capacitor/core": "^8.0.0",
|
|
51
|
+
"@capacitor/docgen": "^0.3.1",
|
|
52
|
+
"@capacitor/ios": "^8.0.0",
|
|
53
|
+
"@ionic/eslint-config": "^0.4.0",
|
|
54
|
+
"@ionic/prettier-config": "^4.0.0",
|
|
55
|
+
"@ionic/swiftlint-config": "^2.0.0",
|
|
56
|
+
"eslint": "^8.57.1",
|
|
57
|
+
"prettier": "^3.6.2",
|
|
58
|
+
"prettier-plugin-java": "^2.7.7",
|
|
59
|
+
"rimraf": "^6.1.0",
|
|
60
|
+
"rollup": "^4.53.2",
|
|
61
|
+
"swiftlint": "^2.0.0",
|
|
62
|
+
"typescript": "^5.9.3"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@capacitor/core": ">=8.0.0"
|
|
66
|
+
},
|
|
67
|
+
"prettier": "@ionic/prettier-config",
|
|
68
|
+
"swiftlint": "@ionic/swiftlint-config",
|
|
69
|
+
"eslintConfig": {
|
|
70
|
+
"extends": "@ionic/eslint-config/recommended"
|
|
71
|
+
},
|
|
72
|
+
"capacitor": {
|
|
73
|
+
"ios": {
|
|
74
|
+
"src": "ios"
|
|
75
|
+
},
|
|
76
|
+
"android": {
|
|
77
|
+
"src": "android"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|