capacitor-sso-webview 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/CapacitorSsoWebview.podspec +17 -0
- package/Package.swift +28 -0
- package/README.md +141 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/telkomsel/ssowebview/SSOWebView.java +158 -0
- package/android/src/main/java/com/telkomsel/ssowebview/SSOWebViewPlugin.java +62 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +184 -0
- package/dist/esm/definitions.d.ts +33 -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 +8 -0
- package/dist/esm/web.js +47 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +61 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +64 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/SSOWebViewPlugin/SSOWebView.swift +8 -0
- package/ios/Sources/SSOWebViewPlugin/SSOWebViewPlugin.swift +23 -0
- package/ios/Tests/SSOWebViewPluginTests/SSOWebViewTests.swift +15 -0
- package/package.json +80 -0
|
@@ -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 = 'CapacitorSsoWebview'
|
|
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
|
package/Package.swift
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "CapacitorSsoWebview",
|
|
6
|
+
platforms: [.iOS(.v15)],
|
|
7
|
+
products: [
|
|
8
|
+
.library(
|
|
9
|
+
name: "CapacitorSsoWebview",
|
|
10
|
+
targets: ["SSOWebViewPlugin"])
|
|
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: "SSOWebViewPlugin",
|
|
18
|
+
dependencies: [
|
|
19
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
20
|
+
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
21
|
+
],
|
|
22
|
+
path: "ios/Sources/SSOWebViewPlugin"),
|
|
23
|
+
.testTarget(
|
|
24
|
+
name: "SSOWebViewPluginTests",
|
|
25
|
+
dependencies: ["SSOWebViewPlugin"],
|
|
26
|
+
path: "ios/Tests/SSOWebViewPluginTests")
|
|
27
|
+
]
|
|
28
|
+
)
|
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# capacitor-sso-webview
|
|
2
|
+
|
|
3
|
+
SSO WebView with URL intercept
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
To use npm
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install capacitor-sso-webview
|
|
11
|
+
````
|
|
12
|
+
|
|
13
|
+
To use yarn
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yarn add capacitor-sso-webview
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Sync native files
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx cap sync
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
<docgen-index>
|
|
28
|
+
|
|
29
|
+
* [`openSSO(...)`](#opensso)
|
|
30
|
+
* [`close()`](#close)
|
|
31
|
+
* [`addListener('ssoRedirect', ...)`](#addlistenerssoredirect-)
|
|
32
|
+
* [`addListener('ssoCancelled', ...)`](#addlistenerssocancelled-)
|
|
33
|
+
* [`removeAllListeners()`](#removealllisteners)
|
|
34
|
+
* [Interfaces](#interfaces)
|
|
35
|
+
|
|
36
|
+
</docgen-index>
|
|
37
|
+
|
|
38
|
+
<docgen-api>
|
|
39
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
40
|
+
|
|
41
|
+
### openSSO(...)
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
openSSO(options: SSOOpenOptions) => Promise<void>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Buka WebView SSO. Navigasi ke redirectHost akan di-intercept
|
|
48
|
+
dan DIBATALKAN sehingga authorization code tidak dikonsumsi.
|
|
49
|
+
|
|
50
|
+
| Param | Type |
|
|
51
|
+
| ------------- | --------------------------------------------------------- |
|
|
52
|
+
| **`options`** | <code><a href="#ssoopenoptions">SSOOpenOptions</a></code> |
|
|
53
|
+
|
|
54
|
+
--------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### close()
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
close() => Promise<void>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Tutup WebView secara manual
|
|
64
|
+
|
|
65
|
+
--------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
### addListener('ssoRedirect', ...)
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
addListener(eventName: 'ssoRedirect', listenerFunc: (event: SSORedirectEvent) => void) => Promise<PluginListenerHandle>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Fired saat redirect ter-intercept — code masih valid
|
|
75
|
+
|
|
76
|
+
| Param | Type |
|
|
77
|
+
| ------------------ | --------------------------------------------------------------------------------- |
|
|
78
|
+
| **`eventName`** | <code>'ssoRedirect'</code> |
|
|
79
|
+
| **`listenerFunc`** | <code>(event: <a href="#ssoredirectevent">SSORedirectEvent</a>) => void</code> |
|
|
80
|
+
|
|
81
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
82
|
+
|
|
83
|
+
--------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
### addListener('ssoCancelled', ...)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
addListener(eventName: 'ssoCancelled', listenerFunc: () => void) => Promise<PluginListenerHandle>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Fired saat user menutup WebView tanpa menyelesaikan login
|
|
93
|
+
|
|
94
|
+
| Param | Type |
|
|
95
|
+
| ------------------ | --------------------------- |
|
|
96
|
+
| **`eventName`** | <code>'ssoCancelled'</code> |
|
|
97
|
+
| **`listenerFunc`** | <code>() => void</code> |
|
|
98
|
+
|
|
99
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
100
|
+
|
|
101
|
+
--------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
### removeAllListeners()
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
removeAllListeners() => Promise<void>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
--------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
### Interfaces
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
#### SSOOpenOptions
|
|
117
|
+
|
|
118
|
+
| Prop | Type | Description |
|
|
119
|
+
| -------------------------- | ------------------- | ---------------------------------------------------------------- |
|
|
120
|
+
| **`url`** | <code>string</code> | URL SSO authorize endpoint |
|
|
121
|
+
| **`redirectHost`** | <code>string</code> | Host yang menandakan redirect, mis. "tdirectmbl.telkomsel.co.id" |
|
|
122
|
+
| **`redirectPathContains`** | <code>string</code> | Substring path redirect, mis. "mobilelogon" |
|
|
123
|
+
| **`title`** | <code>string</code> | Judul header WebView |
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
#### PluginListenerHandle
|
|
127
|
+
|
|
128
|
+
| Prop | Type |
|
|
129
|
+
| ------------ | ----------------------------------------- |
|
|
130
|
+
| **`remove`** | <code>() => Promise<void></code> |
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
#### SSORedirectEvent
|
|
134
|
+
|
|
135
|
+
| Prop | Type | Description |
|
|
136
|
+
| ----------- | ------------------- | ------------------------------------- |
|
|
137
|
+
| **`url`** | <code>string</code> | Full redirect URL yang ter-intercept |
|
|
138
|
+
| **`code`** | <code>string</code> | Authorization code (belum dikonsumsi) |
|
|
139
|
+
| **`state`** | <code>string</code> | State parameter dari OAuth flow |
|
|
140
|
+
|
|
141
|
+
</docgen-api>
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
}
|
|
7
|
+
|
|
8
|
+
buildscript {
|
|
9
|
+
repositories {
|
|
10
|
+
google()
|
|
11
|
+
mavenCentral()
|
|
12
|
+
}
|
|
13
|
+
dependencies {
|
|
14
|
+
classpath 'com.android.tools.build:gradle:8.13.0'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
apply plugin: 'com.android.library'
|
|
19
|
+
|
|
20
|
+
android {
|
|
21
|
+
namespace = "com.telkomsel.ssowebview"
|
|
22
|
+
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
23
|
+
defaultConfig {
|
|
24
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
|
|
25
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
|
|
26
|
+
versionCode 1
|
|
27
|
+
versionName "1.0"
|
|
28
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
29
|
+
}
|
|
30
|
+
buildTypes {
|
|
31
|
+
release {
|
|
32
|
+
minifyEnabled false
|
|
33
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
lintOptions {
|
|
37
|
+
abortOnError = false
|
|
38
|
+
}
|
|
39
|
+
compileOptions {
|
|
40
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
41
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
repositories {
|
|
47
|
+
google()
|
|
48
|
+
mavenCentral()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
dependencies {
|
|
53
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
54
|
+
implementation project(':capacitor-android')
|
|
55
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
56
|
+
testImplementation "junit:junit:$junitVersion"
|
|
57
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
58
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
59
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
package com.telkomsel.ssowebview;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.app.Dialog;
|
|
5
|
+
import android.graphics.Color;
|
|
6
|
+
import android.net.Uri;
|
|
7
|
+
import android.view.Gravity;
|
|
8
|
+
import android.view.ViewGroup;
|
|
9
|
+
import android.webkit.CookieManager;
|
|
10
|
+
import android.webkit.WebResourceRequest;
|
|
11
|
+
import android.webkit.WebSettings;
|
|
12
|
+
import android.webkit.WebView;
|
|
13
|
+
import android.webkit.WebViewClient;
|
|
14
|
+
import android.widget.LinearLayout;
|
|
15
|
+
import android.widget.TextView;
|
|
16
|
+
|
|
17
|
+
public class SSOWebView {
|
|
18
|
+
|
|
19
|
+
public interface Listener {
|
|
20
|
+
void onRedirectIntercepted(String url, String code, String state);
|
|
21
|
+
void onCancelled();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private Dialog dialog;
|
|
25
|
+
private WebView webView;
|
|
26
|
+
private boolean alreadyRedirected = false;
|
|
27
|
+
|
|
28
|
+
public void open(
|
|
29
|
+
final Activity activity,
|
|
30
|
+
final String url,
|
|
31
|
+
final String redirectHost,
|
|
32
|
+
final String redirectPathContains,
|
|
33
|
+
final String title,
|
|
34
|
+
final Listener listener
|
|
35
|
+
) {
|
|
36
|
+
alreadyRedirected = false;
|
|
37
|
+
|
|
38
|
+
dialog = new Dialog(activity, android.R.style.Theme_Light_NoTitleBar_Fullscreen);
|
|
39
|
+
|
|
40
|
+
LinearLayout root = new LinearLayout(activity);
|
|
41
|
+
root.setOrientation(LinearLayout.VERTICAL);
|
|
42
|
+
root.setLayoutParams(new LinearLayout.LayoutParams(
|
|
43
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
44
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
45
|
+
));
|
|
46
|
+
|
|
47
|
+
/* ── Header bar ── */
|
|
48
|
+
LinearLayout header = new LinearLayout(activity);
|
|
49
|
+
header.setOrientation(LinearLayout.HORIZONTAL);
|
|
50
|
+
header.setBackgroundColor(Color.parseColor("#1A2B4A"));
|
|
51
|
+
header.setPadding(48, 72, 48, 48);
|
|
52
|
+
header.setGravity(Gravity.CENTER_VERTICAL);
|
|
53
|
+
header.setLayoutParams(new LinearLayout.LayoutParams(
|
|
54
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
55
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
56
|
+
));
|
|
57
|
+
|
|
58
|
+
TextView tvTitle = new TextView(activity);
|
|
59
|
+
tvTitle.setText(title != null ? title : "Login");
|
|
60
|
+
tvTitle.setTextColor(Color.WHITE);
|
|
61
|
+
tvTitle.setTextSize(16);
|
|
62
|
+
tvTitle.setLayoutParams(new LinearLayout.LayoutParams(
|
|
63
|
+
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f
|
|
64
|
+
));
|
|
65
|
+
|
|
66
|
+
TextView btnClose = new TextView(activity);
|
|
67
|
+
btnClose.setText("Tutup");
|
|
68
|
+
btnClose.setTextColor(Color.WHITE);
|
|
69
|
+
btnClose.setTextSize(14);
|
|
70
|
+
btnClose.setPadding(32, 16, 16, 16);
|
|
71
|
+
btnClose.setOnClickListener(v -> {
|
|
72
|
+
boolean wasRedirected = alreadyRedirected;
|
|
73
|
+
dismiss();
|
|
74
|
+
if (!wasRedirected && listener != null) listener.onCancelled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
header.addView(tvTitle);
|
|
78
|
+
header.addView(btnClose);
|
|
79
|
+
|
|
80
|
+
/* ── WebView ── */
|
|
81
|
+
webView = new WebView(activity);
|
|
82
|
+
WebSettings s = webView.getSettings();
|
|
83
|
+
s.setJavaScriptEnabled(true);
|
|
84
|
+
s.setDomStorageEnabled(true);
|
|
85
|
+
s.setDatabaseEnabled(true);
|
|
86
|
+
s.setUseWideViewPort(true);
|
|
87
|
+
s.setLoadWithOverviewMode(true);
|
|
88
|
+
s.setSupportZoom(false);
|
|
89
|
+
s.setJavaScriptCanOpenWindowsAutomatically(true);
|
|
90
|
+
|
|
91
|
+
CookieManager cm = CookieManager.getInstance();
|
|
92
|
+
cm.setAcceptCookie(true);
|
|
93
|
+
cm.setAcceptThirdPartyCookies(webView, true);
|
|
94
|
+
|
|
95
|
+
webView.setWebViewClient(new WebViewClient() {
|
|
96
|
+
@Override
|
|
97
|
+
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
|
98
|
+
Uri u = request.getUrl();
|
|
99
|
+
String host = u.getHost();
|
|
100
|
+
String path = u.getPath();
|
|
101
|
+
|
|
102
|
+
boolean isRedirect =
|
|
103
|
+
host != null && host.equalsIgnoreCase(redirectHost) &&
|
|
104
|
+
path != null && path.toLowerCase()
|
|
105
|
+
.contains(redirectPathContains.toLowerCase());
|
|
106
|
+
|
|
107
|
+
if (isRedirect && !alreadyRedirected) {
|
|
108
|
+
alreadyRedirected = true;
|
|
109
|
+
|
|
110
|
+
String code = u.getQueryParameter("code");
|
|
111
|
+
String state = u.getQueryParameter("state");
|
|
112
|
+
|
|
113
|
+
dismiss();
|
|
114
|
+
|
|
115
|
+
if (listener != null) {
|
|
116
|
+
listener.onRedirectIntercepted(
|
|
117
|
+
u.toString(),
|
|
118
|
+
code != null ? code : "",
|
|
119
|
+
state != null ? state : ""
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return true; /* BLOKIR navigasi — code TIDAK dikonsumsi */
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
webView.setLayoutParams(new LinearLayout.LayoutParams(
|
|
129
|
+
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f
|
|
130
|
+
));
|
|
131
|
+
|
|
132
|
+
root.addView(header);
|
|
133
|
+
root.addView(webView);
|
|
134
|
+
|
|
135
|
+
dialog.setContentView(root);
|
|
136
|
+
dialog.setCancelable(true);
|
|
137
|
+
dialog.setOnCancelListener(d -> {
|
|
138
|
+
if (!alreadyRedirected && listener != null) listener.onCancelled();
|
|
139
|
+
});
|
|
140
|
+
dialog.show();
|
|
141
|
+
|
|
142
|
+
webView.loadUrl(url);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public void dismiss() {
|
|
146
|
+
try {
|
|
147
|
+
if (webView != null) {
|
|
148
|
+
webView.stopLoading();
|
|
149
|
+
webView.destroy();
|
|
150
|
+
webView = null;
|
|
151
|
+
}
|
|
152
|
+
if (dialog != null && dialog.isShowing()) {
|
|
153
|
+
dialog.dismiss();
|
|
154
|
+
}
|
|
155
|
+
dialog = null;
|
|
156
|
+
} catch (Exception ignored) {}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package com.telkomsel.ssowebview;
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.JSObject;
|
|
4
|
+
import com.getcapacitor.Plugin;
|
|
5
|
+
import com.getcapacitor.PluginCall;
|
|
6
|
+
import com.getcapacitor.PluginMethod;
|
|
7
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
8
|
+
|
|
9
|
+
@CapacitorPlugin(name = "SSOWebView")
|
|
10
|
+
public class SSOWebViewPlugin extends Plugin {
|
|
11
|
+
|
|
12
|
+
private final SSOWebView implementation = new SSOWebView();
|
|
13
|
+
|
|
14
|
+
@PluginMethod
|
|
15
|
+
public void openSSO(PluginCall call) {
|
|
16
|
+
final String url = call.getString("url");
|
|
17
|
+
final String redirectHost = call.getString("redirectHost", "tdirectmbl.telkomsel.co.id");
|
|
18
|
+
final String redirectPath = call.getString("redirectPathContains", "mobilelogon");
|
|
19
|
+
final String title = call.getString("title", "Login");
|
|
20
|
+
|
|
21
|
+
if (url == null || url.isEmpty()) {
|
|
22
|
+
call.reject("url is required");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getActivity().runOnUiThread(() -> {
|
|
27
|
+
try {
|
|
28
|
+
implementation.open(
|
|
29
|
+
getActivity(),
|
|
30
|
+
url,
|
|
31
|
+
redirectHost,
|
|
32
|
+
redirectPath,
|
|
33
|
+
title,
|
|
34
|
+
new SSOWebView.Listener() {
|
|
35
|
+
@Override
|
|
36
|
+
public void onRedirectIntercepted(String u, String code, String state) {
|
|
37
|
+
JSObject ret = new JSObject();
|
|
38
|
+
ret.put("url", u);
|
|
39
|
+
ret.put("code", code);
|
|
40
|
+
ret.put("state", state);
|
|
41
|
+
notifyListeners("ssoRedirect", ret);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Override
|
|
45
|
+
public void onCancelled() {
|
|
46
|
+
notifyListeners("ssoCancelled", new JSObject());
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
call.resolve();
|
|
51
|
+
} catch (Exception e) {
|
|
52
|
+
call.reject("Failed to open SSO WebView: " + e.getMessage());
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@PluginMethod
|
|
58
|
+
public void close(PluginCall call) {
|
|
59
|
+
getActivity().runOnUiThread(implementation::dismiss);
|
|
60
|
+
call.resolve();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
File without changes
|
package/dist/docs.json
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
{
|
|
2
|
+
"api": {
|
|
3
|
+
"name": "SSOWebViewPlugin",
|
|
4
|
+
"slug": "ssowebviewplugin",
|
|
5
|
+
"docs": "",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"methods": [
|
|
8
|
+
{
|
|
9
|
+
"name": "openSSO",
|
|
10
|
+
"signature": "(options: SSOOpenOptions) => Promise<void>",
|
|
11
|
+
"parameters": [
|
|
12
|
+
{
|
|
13
|
+
"name": "options",
|
|
14
|
+
"docs": "",
|
|
15
|
+
"type": "SSOOpenOptions"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"returns": "Promise<void>",
|
|
19
|
+
"tags": [],
|
|
20
|
+
"docs": "Buka WebView SSO. Navigasi ke redirectHost akan di-intercept\ndan DIBATALKAN sehingga authorization code tidak dikonsumsi.",
|
|
21
|
+
"complexTypes": [
|
|
22
|
+
"SSOOpenOptions"
|
|
23
|
+
],
|
|
24
|
+
"slug": "opensso"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "close",
|
|
28
|
+
"signature": "() => Promise<void>",
|
|
29
|
+
"parameters": [],
|
|
30
|
+
"returns": "Promise<void>",
|
|
31
|
+
"tags": [],
|
|
32
|
+
"docs": "Tutup WebView secara manual",
|
|
33
|
+
"complexTypes": [],
|
|
34
|
+
"slug": "close"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "addListener",
|
|
38
|
+
"signature": "(eventName: 'ssoRedirect', listenerFunc: (event: SSORedirectEvent) => void) => Promise<PluginListenerHandle>",
|
|
39
|
+
"parameters": [
|
|
40
|
+
{
|
|
41
|
+
"name": "eventName",
|
|
42
|
+
"docs": "",
|
|
43
|
+
"type": "'ssoRedirect'"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "listenerFunc",
|
|
47
|
+
"docs": "",
|
|
48
|
+
"type": "(event: SSORedirectEvent) => void"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
52
|
+
"tags": [],
|
|
53
|
+
"docs": "Fired saat redirect ter-intercept — code masih valid",
|
|
54
|
+
"complexTypes": [
|
|
55
|
+
"PluginListenerHandle",
|
|
56
|
+
"SSORedirectEvent"
|
|
57
|
+
],
|
|
58
|
+
"slug": "addlistenerssoredirect-"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "addListener",
|
|
62
|
+
"signature": "(eventName: 'ssoCancelled', listenerFunc: () => void) => Promise<PluginListenerHandle>",
|
|
63
|
+
"parameters": [
|
|
64
|
+
{
|
|
65
|
+
"name": "eventName",
|
|
66
|
+
"docs": "",
|
|
67
|
+
"type": "'ssoCancelled'"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "listenerFunc",
|
|
71
|
+
"docs": "",
|
|
72
|
+
"type": "() => void"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
76
|
+
"tags": [],
|
|
77
|
+
"docs": "Fired saat user menutup WebView tanpa menyelesaikan login",
|
|
78
|
+
"complexTypes": [
|
|
79
|
+
"PluginListenerHandle"
|
|
80
|
+
],
|
|
81
|
+
"slug": "addlistenerssocancelled-"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "removeAllListeners",
|
|
85
|
+
"signature": "() => Promise<void>",
|
|
86
|
+
"parameters": [],
|
|
87
|
+
"returns": "Promise<void>",
|
|
88
|
+
"tags": [],
|
|
89
|
+
"docs": "",
|
|
90
|
+
"complexTypes": [],
|
|
91
|
+
"slug": "removealllisteners"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"properties": []
|
|
95
|
+
},
|
|
96
|
+
"interfaces": [
|
|
97
|
+
{
|
|
98
|
+
"name": "SSOOpenOptions",
|
|
99
|
+
"slug": "ssoopenoptions",
|
|
100
|
+
"docs": "",
|
|
101
|
+
"tags": [],
|
|
102
|
+
"methods": [],
|
|
103
|
+
"properties": [
|
|
104
|
+
{
|
|
105
|
+
"name": "url",
|
|
106
|
+
"tags": [],
|
|
107
|
+
"docs": "URL SSO authorize endpoint",
|
|
108
|
+
"complexTypes": [],
|
|
109
|
+
"type": "string"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "redirectHost",
|
|
113
|
+
"tags": [],
|
|
114
|
+
"docs": "Host yang menandakan redirect, mis. \"tdirectmbl.telkomsel.co.id\"",
|
|
115
|
+
"complexTypes": [],
|
|
116
|
+
"type": "string"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "redirectPathContains",
|
|
120
|
+
"tags": [],
|
|
121
|
+
"docs": "Substring path redirect, mis. \"mobilelogon\"",
|
|
122
|
+
"complexTypes": [],
|
|
123
|
+
"type": "string"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "title",
|
|
127
|
+
"tags": [],
|
|
128
|
+
"docs": "Judul header WebView",
|
|
129
|
+
"complexTypes": [],
|
|
130
|
+
"type": "string | undefined"
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "PluginListenerHandle",
|
|
136
|
+
"slug": "pluginlistenerhandle",
|
|
137
|
+
"docs": "",
|
|
138
|
+
"tags": [],
|
|
139
|
+
"methods": [],
|
|
140
|
+
"properties": [
|
|
141
|
+
{
|
|
142
|
+
"name": "remove",
|
|
143
|
+
"tags": [],
|
|
144
|
+
"docs": "",
|
|
145
|
+
"complexTypes": [],
|
|
146
|
+
"type": "() => Promise<void>"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "SSORedirectEvent",
|
|
152
|
+
"slug": "ssoredirectevent",
|
|
153
|
+
"docs": "",
|
|
154
|
+
"tags": [],
|
|
155
|
+
"methods": [],
|
|
156
|
+
"properties": [
|
|
157
|
+
{
|
|
158
|
+
"name": "url",
|
|
159
|
+
"tags": [],
|
|
160
|
+
"docs": "Full redirect URL yang ter-intercept",
|
|
161
|
+
"complexTypes": [],
|
|
162
|
+
"type": "string"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "code",
|
|
166
|
+
"tags": [],
|
|
167
|
+
"docs": "Authorization code (belum dikonsumsi)",
|
|
168
|
+
"complexTypes": [],
|
|
169
|
+
"type": "string"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "state",
|
|
173
|
+
"tags": [],
|
|
174
|
+
"docs": "State parameter dari OAuth flow",
|
|
175
|
+
"complexTypes": [],
|
|
176
|
+
"type": "string"
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
"enums": [],
|
|
182
|
+
"typeAliases": [],
|
|
183
|
+
"pluginConfigs": []
|
|
184
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PluginListenerHandle } from '@capacitor/core';
|
|
2
|
+
export interface SSORedirectEvent {
|
|
3
|
+
/** Full redirect URL yang ter-intercept */
|
|
4
|
+
url: string;
|
|
5
|
+
/** Authorization code (belum dikonsumsi) */
|
|
6
|
+
code: string;
|
|
7
|
+
/** State parameter dari OAuth flow */
|
|
8
|
+
state: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SSOOpenOptions {
|
|
11
|
+
/** URL SSO authorize endpoint */
|
|
12
|
+
url: string;
|
|
13
|
+
/** Host yang menandakan redirect, mis. "tdirectmbl.telkomsel.co.id" */
|
|
14
|
+
redirectHost: string;
|
|
15
|
+
/** Substring path redirect, mis. "mobilelogon" */
|
|
16
|
+
redirectPathContains: string;
|
|
17
|
+
/** Judul header WebView */
|
|
18
|
+
title?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SSOWebViewPlugin {
|
|
21
|
+
/**
|
|
22
|
+
* Buka WebView SSO. Navigasi ke redirectHost akan di-intercept
|
|
23
|
+
* dan DIBATALKAN sehingga authorization code tidak dikonsumsi.
|
|
24
|
+
*/
|
|
25
|
+
openSSO(options: SSOOpenOptions): Promise<void>;
|
|
26
|
+
/** Tutup WebView secara manual */
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
/** Fired saat redirect ter-intercept — code masih valid */
|
|
29
|
+
addListener(eventName: 'ssoRedirect', listenerFunc: (event: SSORedirectEvent) => void): Promise<PluginListenerHandle>;
|
|
30
|
+
/** Fired saat user menutup WebView tanpa menyelesaikan login */
|
|
31
|
+
addListener(eventName: 'ssoCancelled', listenerFunc: () => void): Promise<PluginListenerHandle>;
|
|
32
|
+
removeAllListeners(): Promise<void>;
|
|
33
|
+
}
|
|
@@ -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 SSORedirectEvent {\n /** Full redirect URL yang ter-intercept */\n url: string;\n /** Authorization code (belum dikonsumsi) */\n code: string;\n /** State parameter dari OAuth flow */\n state: string;\n}\n\nexport interface SSOOpenOptions {\n /** URL SSO authorize endpoint */\n url: string;\n /** Host yang menandakan redirect, mis. \"tdirectmbl.telkomsel.co.id\" */\n redirectHost: string;\n /** Substring path redirect, mis. \"mobilelogon\" */\n redirectPathContains: string;\n /** Judul header WebView */\n title?: string;\n}\n\nexport interface SSOWebViewPlugin {\n /**\n * Buka WebView SSO. Navigasi ke redirectHost akan di-intercept\n * dan DIBATALKAN sehingga authorization code tidak dikonsumsi.\n */\n openSSO(options: SSOOpenOptions): Promise<void>;\n\n /** Tutup WebView secara manual */\n close(): Promise<void>;\n\n /** Fired saat redirect ter-intercept — code masih valid */\n addListener(eventName: 'ssoRedirect', listenerFunc: (event: SSORedirectEvent) => void): Promise<PluginListenerHandle>;\n\n /** Fired saat user menutup WebView tanpa menyelesaikan login */\n addListener(eventName: 'ssoCancelled', listenerFunc: () => void): Promise<PluginListenerHandle>;\n\n removeAllListeners(): Promise<void>;\n}\n"]}
|
|
@@ -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,UAAU,GAAG,cAAc,CAAmB,YAAY,EAAE;IAChE,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;CAC9D,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\n\nimport type { SSOWebViewPlugin } from './definitions';\n\nconst SSOWebView = registerPlugin<SSOWebViewPlugin>('SSOWebView', {\n web: () => import('./web').then((m) => new m.SSOWebViewWeb()),\n});\n\nexport * from './definitions';\nexport { SSOWebView };\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { WebPlugin } from '@capacitor/core';
|
|
2
|
+
import type { SSOOpenOptions, SSOWebViewPlugin } from './definitions';
|
|
3
|
+
export declare class SSOWebViewWeb extends WebPlugin implements SSOWebViewPlugin {
|
|
4
|
+
private popup;
|
|
5
|
+
private pollTimer;
|
|
6
|
+
openSSO(options: SSOOpenOptions): Promise<void>;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
}
|
package/dist/esm/web.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { WebPlugin } from '@capacitor/core';
|
|
2
|
+
export class SSOWebViewWeb extends WebPlugin {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.popup = null;
|
|
6
|
+
this.pollTimer = null;
|
|
7
|
+
}
|
|
8
|
+
async openSSO(options) {
|
|
9
|
+
this.popup = window.open(options.url, 'ssoLogin', 'width=480,height=680,toolbar=1,location=1,scrollbars=1,resizable=1');
|
|
10
|
+
if (!this.popup) {
|
|
11
|
+
throw new Error('Popup blocked by browser');
|
|
12
|
+
}
|
|
13
|
+
// Web: polling terbatas oleh CORS, hanya untuk same-origin redirect
|
|
14
|
+
this.pollTimer = setInterval(() => {
|
|
15
|
+
var _a, _b;
|
|
16
|
+
try {
|
|
17
|
+
if (!this.popup || this.popup.closed) {
|
|
18
|
+
clearInterval(this.pollTimer);
|
|
19
|
+
this.notifyListeners('ssoCancelled', {});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const href = this.popup.location.href;
|
|
23
|
+
if (href.includes(options.redirectPathContains)) {
|
|
24
|
+
clearInterval(this.pollTimer);
|
|
25
|
+
const u = new URL(href);
|
|
26
|
+
this.notifyListeners('ssoRedirect', {
|
|
27
|
+
url: href,
|
|
28
|
+
code: (_a = u.searchParams.get('code')) !== null && _a !== void 0 ? _a : '',
|
|
29
|
+
state: (_b = u.searchParams.get('state')) !== null && _b !== void 0 ? _b : '',
|
|
30
|
+
});
|
|
31
|
+
this.popup.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (_c) {
|
|
35
|
+
// cross-origin — normal selama masih di domain SSO
|
|
36
|
+
}
|
|
37
|
+
}, 300);
|
|
38
|
+
}
|
|
39
|
+
async close() {
|
|
40
|
+
if (this.pollTimer)
|
|
41
|
+
clearInterval(this.pollTimer);
|
|
42
|
+
if (this.popup && !this.popup.closed)
|
|
43
|
+
this.popup.close();
|
|
44
|
+
this.popup = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# 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,aAAc,SAAQ,SAAS;IAA5C;;QACU,UAAK,GAAkB,IAAI,CAAC;QAC5B,cAAS,GAAQ,IAAI,CAAC;IA2ChC,CAAC;IAzCC,KAAK,CAAC,OAAO,CAAC,OAAuB;QACnC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CACtB,OAAO,CAAC,GAAG,EACX,UAAU,EACV,oEAAoE,CACrE,CAAC;QAEF,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC9C,CAAC;QAED,oEAAoE;QACpE,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE;;YAChC,IAAI,CAAC;gBACH,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;oBACrC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;oBAC9B,IAAI,CAAC,eAAe,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;oBACzC,OAAO;gBACT,CAAC;gBACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;gBACtC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,oBAAoB,CAAC,EAAE,CAAC;oBAChD,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;oBAC9B,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;oBACxB,IAAI,CAAC,eAAe,CAAC,aAAa,EAAE;wBAClC,GAAG,EAAE,IAAI;wBACT,IAAI,EAAE,MAAA,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,mCAAI,EAAE;wBACtC,KAAK,EAAE,MAAA,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,mCAAI,EAAE;qBACzC,CAAC,CAAC;oBACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;gBACrB,CAAC;YACH,CAAC;YAAC,WAAM,CAAC;gBACP,mDAAmD;YACrD,CAAC;QACH,CAAC,EAAE,GAAG,CAAC,CAAC;IACV,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,SAAS;YAAE,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAClD,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM;YAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACzD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { SSOOpenOptions, SSOWebViewPlugin } from './definitions';\n\nexport class SSOWebViewWeb extends WebPlugin implements SSOWebViewPlugin {\n private popup: Window | null = null;\n private pollTimer: any = null;\n\n async openSSO(options: SSOOpenOptions): Promise<void> {\n this.popup = window.open(\n options.url,\n 'ssoLogin',\n 'width=480,height=680,toolbar=1,location=1,scrollbars=1,resizable=1',\n );\n\n if (!this.popup) {\n throw new Error('Popup blocked by browser');\n }\n\n // Web: polling terbatas oleh CORS, hanya untuk same-origin redirect\n this.pollTimer = setInterval(() => {\n try {\n if (!this.popup || this.popup.closed) {\n clearInterval(this.pollTimer);\n this.notifyListeners('ssoCancelled', {});\n return;\n }\n const href = this.popup.location.href;\n if (href.includes(options.redirectPathContains)) {\n clearInterval(this.pollTimer);\n const u = new URL(href);\n this.notifyListeners('ssoRedirect', {\n url: href,\n code: u.searchParams.get('code') ?? '',\n state: u.searchParams.get('state') ?? '',\n });\n this.popup.close();\n }\n } catch {\n // cross-origin — normal selama masih di domain SSO\n }\n }, 300);\n }\n\n async close(): Promise<void> {\n if (this.pollTimer) clearInterval(this.pollTimer);\n if (this.popup && !this.popup.closed) this.popup.close();\n this.popup = null;\n }\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@capacitor/core');
|
|
4
|
+
|
|
5
|
+
const SSOWebView = core.registerPlugin('SSOWebView', {
|
|
6
|
+
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.SSOWebViewWeb()),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
class SSOWebViewWeb extends core.WebPlugin {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
this.popup = null;
|
|
13
|
+
this.pollTimer = null;
|
|
14
|
+
}
|
|
15
|
+
async openSSO(options) {
|
|
16
|
+
this.popup = window.open(options.url, 'ssoLogin', 'width=480,height=680,toolbar=1,location=1,scrollbars=1,resizable=1');
|
|
17
|
+
if (!this.popup) {
|
|
18
|
+
throw new Error('Popup blocked by browser');
|
|
19
|
+
}
|
|
20
|
+
// Web: polling terbatas oleh CORS, hanya untuk same-origin redirect
|
|
21
|
+
this.pollTimer = setInterval(() => {
|
|
22
|
+
var _a, _b;
|
|
23
|
+
try {
|
|
24
|
+
if (!this.popup || this.popup.closed) {
|
|
25
|
+
clearInterval(this.pollTimer);
|
|
26
|
+
this.notifyListeners('ssoCancelled', {});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const href = this.popup.location.href;
|
|
30
|
+
if (href.includes(options.redirectPathContains)) {
|
|
31
|
+
clearInterval(this.pollTimer);
|
|
32
|
+
const u = new URL(href);
|
|
33
|
+
this.notifyListeners('ssoRedirect', {
|
|
34
|
+
url: href,
|
|
35
|
+
code: (_a = u.searchParams.get('code')) !== null && _a !== void 0 ? _a : '',
|
|
36
|
+
state: (_b = u.searchParams.get('state')) !== null && _b !== void 0 ? _b : '',
|
|
37
|
+
});
|
|
38
|
+
this.popup.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (_c) {
|
|
42
|
+
// cross-origin — normal selama masih di domain SSO
|
|
43
|
+
}
|
|
44
|
+
}, 300);
|
|
45
|
+
}
|
|
46
|
+
async close() {
|
|
47
|
+
if (this.pollTimer)
|
|
48
|
+
clearInterval(this.pollTimer);
|
|
49
|
+
if (this.popup && !this.popup.closed)
|
|
50
|
+
this.popup.close();
|
|
51
|
+
this.popup = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
56
|
+
__proto__: null,
|
|
57
|
+
SSOWebViewWeb: SSOWebViewWeb
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
exports.SSOWebView = SSOWebView;
|
|
61
|
+
//# 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 SSOWebView = registerPlugin('SSOWebView', {\n web: () => import('./web').then((m) => new m.SSOWebViewWeb()),\n});\nexport * from './definitions';\nexport { SSOWebView };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SSOWebViewWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.popup = null;\n this.pollTimer = null;\n }\n async openSSO(options) {\n this.popup = window.open(options.url, 'ssoLogin', 'width=480,height=680,toolbar=1,location=1,scrollbars=1,resizable=1');\n if (!this.popup) {\n throw new Error('Popup blocked by browser');\n }\n // Web: polling terbatas oleh CORS, hanya untuk same-origin redirect\n this.pollTimer = setInterval(() => {\n var _a, _b;\n try {\n if (!this.popup || this.popup.closed) {\n clearInterval(this.pollTimer);\n this.notifyListeners('ssoCancelled', {});\n return;\n }\n const href = this.popup.location.href;\n if (href.includes(options.redirectPathContains)) {\n clearInterval(this.pollTimer);\n const u = new URL(href);\n this.notifyListeners('ssoRedirect', {\n url: href,\n code: (_a = u.searchParams.get('code')) !== null && _a !== void 0 ? _a : '',\n state: (_b = u.searchParams.get('state')) !== null && _b !== void 0 ? _b : '',\n });\n this.popup.close();\n }\n }\n catch (_c) {\n // cross-origin — normal selama masih di domain SSO\n }\n }, 300);\n }\n async close() {\n if (this.pollTimer)\n clearInterval(this.pollTimer);\n if (this.popup && !this.popup.closed)\n this.popup.close();\n this.popup = null;\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,UAAU,GAAGA,mBAAc,CAAC,YAAY,EAAE;AAChD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;AACjE,CAAC;;ACFM,MAAM,aAAa,SAASC,cAAS,CAAC;AAC7C,IAAI,WAAW,GAAG;AAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;AAC3B,QAAQ,IAAI,CAAC,KAAK,GAAG,IAAI;AACzB,QAAQ,IAAI,CAAC,SAAS,GAAG,IAAI;AAC7B,IAAI;AACJ,IAAI,MAAM,OAAO,CAAC,OAAO,EAAE;AAC3B,QAAQ,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,EAAE,oEAAoE,CAAC;AAC/H,QAAQ,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE;AACzB,YAAY,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC;AACvD,QAAQ;AACR;AACA,QAAQ,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,MAAM;AAC3C,YAAY,IAAI,EAAE,EAAE,EAAE;AACtB,YAAY,IAAI;AAChB,gBAAgB,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;AACtD,oBAAoB,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC;AACjD,oBAAoB,IAAI,CAAC,eAAe,CAAC,cAAc,EAAE,EAAE,CAAC;AAC5D,oBAAoB;AACpB,gBAAgB;AAChB,gBAAgB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI;AACrD,gBAAgB,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,oBAAoB,CAAC,EAAE;AACjE,oBAAoB,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC;AACjD,oBAAoB,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC;AAC3C,oBAAoB,IAAI,CAAC,eAAe,CAAC,aAAa,EAAE;AACxD,wBAAwB,GAAG,EAAE,IAAI;AACjC,wBAAwB,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,EAAE,KAAK,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE;AACnG,wBAAwB,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,EAAE,KAAK,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE;AACrG,qBAAqB,CAAC;AACtB,oBAAoB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;AACtC,gBAAgB;AAChB,YAAY;AACZ,YAAY,OAAO,EAAE,EAAE;AACvB;AACA,YAAY;AACZ,QAAQ,CAAC,EAAE,GAAG,CAAC;AACf,IAAI;AACJ,IAAI,MAAM,KAAK,GAAG;AAClB,QAAQ,IAAI,IAAI,CAAC,SAAS;AAC1B,YAAY,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC;AACzC,QAAQ,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM;AAC5C,YAAY,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;AAC9B,QAAQ,IAAI,CAAC,KAAK,GAAG,IAAI;AACzB,IAAI;AACJ;;;;;;;"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
var capacitorSSOWebView = (function (exports, core) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const SSOWebView = core.registerPlugin('SSOWebView', {
|
|
5
|
+
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.SSOWebViewWeb()),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
class SSOWebViewWeb extends core.WebPlugin {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.popup = null;
|
|
12
|
+
this.pollTimer = null;
|
|
13
|
+
}
|
|
14
|
+
async openSSO(options) {
|
|
15
|
+
this.popup = window.open(options.url, 'ssoLogin', 'width=480,height=680,toolbar=1,location=1,scrollbars=1,resizable=1');
|
|
16
|
+
if (!this.popup) {
|
|
17
|
+
throw new Error('Popup blocked by browser');
|
|
18
|
+
}
|
|
19
|
+
// Web: polling terbatas oleh CORS, hanya untuk same-origin redirect
|
|
20
|
+
this.pollTimer = setInterval(() => {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
try {
|
|
23
|
+
if (!this.popup || this.popup.closed) {
|
|
24
|
+
clearInterval(this.pollTimer);
|
|
25
|
+
this.notifyListeners('ssoCancelled', {});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const href = this.popup.location.href;
|
|
29
|
+
if (href.includes(options.redirectPathContains)) {
|
|
30
|
+
clearInterval(this.pollTimer);
|
|
31
|
+
const u = new URL(href);
|
|
32
|
+
this.notifyListeners('ssoRedirect', {
|
|
33
|
+
url: href,
|
|
34
|
+
code: (_a = u.searchParams.get('code')) !== null && _a !== void 0 ? _a : '',
|
|
35
|
+
state: (_b = u.searchParams.get('state')) !== null && _b !== void 0 ? _b : '',
|
|
36
|
+
});
|
|
37
|
+
this.popup.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (_c) {
|
|
41
|
+
// cross-origin — normal selama masih di domain SSO
|
|
42
|
+
}
|
|
43
|
+
}, 300);
|
|
44
|
+
}
|
|
45
|
+
async close() {
|
|
46
|
+
if (this.pollTimer)
|
|
47
|
+
clearInterval(this.pollTimer);
|
|
48
|
+
if (this.popup && !this.popup.closed)
|
|
49
|
+
this.popup.close();
|
|
50
|
+
this.popup = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
55
|
+
__proto__: null,
|
|
56
|
+
SSOWebViewWeb: SSOWebViewWeb
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
exports.SSOWebView = SSOWebView;
|
|
60
|
+
|
|
61
|
+
return exports;
|
|
62
|
+
|
|
63
|
+
})({}, capacitorExports);
|
|
64
|
+
//# 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 SSOWebView = registerPlugin('SSOWebView', {\n web: () => import('./web').then((m) => new m.SSOWebViewWeb()),\n});\nexport * from './definitions';\nexport { SSOWebView };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SSOWebViewWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.popup = null;\n this.pollTimer = null;\n }\n async openSSO(options) {\n this.popup = window.open(options.url, 'ssoLogin', 'width=480,height=680,toolbar=1,location=1,scrollbars=1,resizable=1');\n if (!this.popup) {\n throw new Error('Popup blocked by browser');\n }\n // Web: polling terbatas oleh CORS, hanya untuk same-origin redirect\n this.pollTimer = setInterval(() => {\n var _a, _b;\n try {\n if (!this.popup || this.popup.closed) {\n clearInterval(this.pollTimer);\n this.notifyListeners('ssoCancelled', {});\n return;\n }\n const href = this.popup.location.href;\n if (href.includes(options.redirectPathContains)) {\n clearInterval(this.pollTimer);\n const u = new URL(href);\n this.notifyListeners('ssoRedirect', {\n url: href,\n code: (_a = u.searchParams.get('code')) !== null && _a !== void 0 ? _a : '',\n state: (_b = u.searchParams.get('state')) !== null && _b !== void 0 ? _b : '',\n });\n this.popup.close();\n }\n }\n catch (_c) {\n // cross-origin — normal selama masih di domain SSO\n }\n }, 300);\n }\n async close() {\n if (this.pollTimer)\n clearInterval(this.pollTimer);\n if (this.popup && !this.popup.closed)\n this.popup.close();\n this.popup = null;\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,UAAU,GAAGA,mBAAc,CAAC,YAAY,EAAE;IAChD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;IACjE,CAAC;;ICFM,MAAM,aAAa,SAASC,cAAS,CAAC;IAC7C,IAAI,WAAW,GAAG;IAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;IAC3B,QAAQ,IAAI,CAAC,KAAK,GAAG,IAAI;IACzB,QAAQ,IAAI,CAAC,SAAS,GAAG,IAAI;IAC7B,IAAI;IACJ,IAAI,MAAM,OAAO,CAAC,OAAO,EAAE;IAC3B,QAAQ,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,EAAE,oEAAoE,CAAC;IAC/H,QAAQ,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE;IACzB,YAAY,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC;IACvD,QAAQ;IACR;IACA,QAAQ,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,MAAM;IAC3C,YAAY,IAAI,EAAE,EAAE,EAAE;IACtB,YAAY,IAAI;IAChB,gBAAgB,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;IACtD,oBAAoB,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC;IACjD,oBAAoB,IAAI,CAAC,eAAe,CAAC,cAAc,EAAE,EAAE,CAAC;IAC5D,oBAAoB;IACpB,gBAAgB;IAChB,gBAAgB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI;IACrD,gBAAgB,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,oBAAoB,CAAC,EAAE;IACjE,oBAAoB,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC;IACjD,oBAAoB,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC;IAC3C,oBAAoB,IAAI,CAAC,eAAe,CAAC,aAAa,EAAE;IACxD,wBAAwB,GAAG,EAAE,IAAI;IACjC,wBAAwB,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,EAAE,KAAK,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE;IACnG,wBAAwB,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,EAAE,KAAK,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE;IACrG,qBAAqB,CAAC;IACtB,oBAAoB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;IACtC,gBAAgB;IAChB,YAAY;IACZ,YAAY,OAAO,EAAE,EAAE;IACvB;IACA,YAAY;IACZ,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,IAAI;IACJ,IAAI,MAAM,KAAK,GAAG;IAClB,QAAQ,IAAI,IAAI,CAAC,SAAS;IAC1B,YAAY,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC;IACzC,QAAQ,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM;IAC5C,YAAY,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;IAC9B,QAAQ,IAAI,CAAC,KAAK,GAAG,IAAI;IACzB,IAAI;IACJ;;;;;;;;;;;"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Please read the Capacitor iOS Plugin Development Guide
|
|
6
|
+
* here: https://capacitorjs.com/docs/plugins/ios
|
|
7
|
+
*/
|
|
8
|
+
@objc(SSOWebViewPlugin)
|
|
9
|
+
public class SSOWebViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
10
|
+
public let identifier = "SSOWebViewPlugin"
|
|
11
|
+
public let jsName = "SSOWebView"
|
|
12
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
13
|
+
CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise)
|
|
14
|
+
]
|
|
15
|
+
private let implementation = SSOWebView()
|
|
16
|
+
|
|
17
|
+
@objc func echo(_ call: CAPPluginCall) {
|
|
18
|
+
let value = call.getString("value") ?? ""
|
|
19
|
+
call.resolve([
|
|
20
|
+
"value": implementation.echo(value)
|
|
21
|
+
])
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import SSOWebViewPlugin
|
|
3
|
+
|
|
4
|
+
class SSOWebViewTests: XCTestCase {
|
|
5
|
+
func testEcho() {
|
|
6
|
+
// This is an example of a functional test case for a plugin.
|
|
7
|
+
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
8
|
+
|
|
9
|
+
let implementation = SSOWebView()
|
|
10
|
+
let value = "Hello, World!"
|
|
11
|
+
let result = implementation.echo(value)
|
|
12
|
+
|
|
13
|
+
XCTAssertEqual(value, result)
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "capacitor-sso-webview",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "SSO WebView with URL intercept",
|
|
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
|
+
"CapacitorSsoWebview.podspec"
|
|
17
|
+
],
|
|
18
|
+
"author": "Telkomsel",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Telkomsel/capacitor-sso-webview.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Telkomsel/capacitor-sso-webview/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 CapacitorSsoWebview -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 SSOWebViewPlugin --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.3.5",
|
|
50
|
+
"@capacitor/core": "^8.3.5",
|
|
51
|
+
"@capacitor/docgen": "^0.3.1",
|
|
52
|
+
"@capacitor/ios": "^8.3.5",
|
|
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": ">=6.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
|
+
}
|