@jolibox/implement 1.1.18 → 1.1.19-beta.6

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.
Files changed (43) hide show
  1. package/.rush/temp/package-deps_build.json +23 -16
  2. package/.rush/temp/shrinkwrap-deps.json +1 -1
  3. package/dist/common/rewards/index.d.ts +1 -10
  4. package/dist/common/rewards/registers/use-ads.d.ts +3 -2
  5. package/dist/common/rewards/registers/use-jolicoin-only.d.ts +10 -0
  6. package/dist/common/rewards/registers/utils/coins/index.d.ts +32 -0
  7. package/dist/common/rewards/registers/utils/event-listener.d.ts +15 -0
  8. package/dist/common/rewards/registers/utils/index.d.ts +1 -0
  9. package/dist/common/rewards/reward-emitter.d.ts +58 -0
  10. package/dist/common/rewards/reward-helper.d.ts +2 -1
  11. package/dist/common/utils/index.d.ts +49 -2
  12. package/dist/h5/api/ads.d.ts +1 -1
  13. package/dist/h5/http/index.d.ts +1 -1
  14. package/dist/h5/rewards/index.d.ts +4 -0
  15. package/dist/index.js +197 -3
  16. package/dist/index.native.js +402 -21
  17. package/dist/native/api/ads.d.ts +1 -1
  18. package/dist/native/api/login.d.ts +4 -1
  19. package/dist/native/rewards/index.d.ts +4 -0
  20. package/dist/native/ui/modal-iframe.d.ts +26 -0
  21. package/implement.build.log +2 -2
  22. package/package.json +5 -4
  23. package/src/common/context/index.ts +3 -3
  24. package/src/common/rewards/fetch-reward.ts +9 -3
  25. package/src/common/rewards/index.ts +1 -12
  26. package/src/common/rewards/registers/use-ads.ts +3 -2
  27. package/src/common/rewards/registers/use-jolicoin-only.ts +57 -0
  28. package/src/common/rewards/registers/use-jolicoin.ts +27 -77
  29. package/src/common/rewards/registers/utils/coins/index.ts +186 -0
  30. package/src/common/rewards/registers/utils/event-listener.ts +66 -0
  31. package/src/common/rewards/registers/utils/index.ts +8 -0
  32. package/src/common/rewards/reward-emitter.ts +79 -0
  33. package/src/common/rewards/reward-helper.ts +3 -2
  34. package/src/common/utils/index.ts +117 -4
  35. package/src/h5/api/ads.ts +65 -12
  36. package/src/h5/http/index.ts +2 -2
  37. package/src/h5/rewards/index.ts +69 -0
  38. package/src/native/api/ads.ts +95 -33
  39. package/src/native/api/login.ts +1 -1
  40. package/src/native/api/navigate.ts +13 -1
  41. package/src/native/bootstrap/index.ts +25 -1
  42. package/src/native/rewards/index.ts +138 -0
  43. package/src/native/ui/modal-iframe.ts +271 -0
@@ -7,6 +7,7 @@ import { adEventEmitter } from '@/common/ads';
7
7
  import { openRetentionSchema } from '../ui/retention';
8
8
  import { innerFetch } from '../network';
9
9
  import { Env } from '@jolibox/types';
10
+ import { createIframeModal, registerIframeModalToGlobal } from '../ui/modal-iframe';
10
11
  interface IBasicMetaConfig {
11
12
  canShowRecommended: boolean;
12
13
  }
@@ -27,6 +28,7 @@ declare const globalThis: {
27
28
  * 清除safari jolibox样式
28
29
  */
29
30
  let cleanStyles: () => void;
31
+ let unregisterIframeModal: () => void;
30
32
 
31
33
  /**
32
34
  * 启动时间戳
@@ -40,6 +42,11 @@ let isAdShowing = false;
40
42
 
41
43
  let baskcMeta: IBasicMetaConfig | null = null;
42
44
 
45
+ /**
46
+ * 是否全局系统退出
47
+ */
48
+ let isInterceptSystemExit = false;
49
+
43
50
  RuntimeLoader.onReady(() => {
44
51
  // TODO: merge some env config
45
52
  });
@@ -59,6 +66,15 @@ function addShowAdListener() {
59
66
  });
60
67
  }
61
68
 
69
+ /**
70
+ * 添加系统退出拦截监听
71
+ */
72
+ function addInterceptSystemExitListener() {
73
+ hostEmitter.on('onInterceptSystemExit', ({ intercept }) => {
74
+ isInterceptSystemExit = intercept;
75
+ });
76
+ }
77
+
62
78
  /**
63
79
  * The DOMContentLoaded event might be triggered very early,
64
80
  * so the global loaded listener should be executed during the bootstrap process.
@@ -121,11 +137,16 @@ function addDoExitLoader() {
121
137
  timestamp: Date.now()
122
138
  });
123
139
  cleanStyles?.();
140
+ unregisterIframeModal?.();
124
141
  taskTracker.close(Date.now() - start_timestamp);
125
142
  };
126
143
 
144
+ const shouldInterceptSystemExit = () => {
145
+ return isAdShowing || isInterceptSystemExit;
146
+ };
147
+
127
148
  RuntimeLoader.onDoExit(async () => {
128
- if (isAdShowing) {
149
+ if (shouldInterceptSystemExit()) {
129
150
  return true; // Forbid exit on watching ads
130
151
  }
131
152
 
@@ -168,9 +189,12 @@ export function config(): void {
168
189
  start_timestamp = Date.now();
169
190
  addGameServiceReadyListener();
170
191
  addShowAdListener();
192
+ addInterceptSystemExitListener();
171
193
  addDoExitLoader();
172
194
  addWebviewReadyListener();
173
195
  addI18nChangedListener();
174
196
  fetchMetaConfig();
197
+ unregisterIframeModal = registerIframeModalToGlobal();
198
+
175
199
  cleanStyles = initializeNativeEnv();
176
200
  }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * rewards event handlers
3
+ */
4
+
5
+ import {
6
+ rewardsEmitter,
7
+ UseModalEventName,
8
+ IUseModalEvent,
9
+ PaymentResultEventName,
10
+ UseModalResultEventName,
11
+ InvokePaymentEventName,
12
+ IInvokePaymentEvent,
13
+ IPaymentChoice
14
+ } from '@/common/rewards/reward-emitter';
15
+ import { createConfirmJolicoinModal, createPaymentJolicoinModal } from '@jolibox/ui';
16
+ import { innerFetch as fetch } from '../network';
17
+ import { StandardResponse } from '@jolibox/types';
18
+ import { context } from '@/common/context';
19
+ import { login } from '../api/login';
20
+
21
+ /**
22
+ * confirm jolicoin modal
23
+ */
24
+ rewardsEmitter.on(UseModalEventName, (type: 'JOLI_COIN' | 'ADS-JOLI_COIN', params: IUseModalEvent) => {
25
+ if (type === 'ADS-JOLI_COIN') {
26
+ //TODO
27
+ console.log('use modal show by frequency');
28
+ }
29
+ const modal = createConfirmJolicoinModal({
30
+ data: {
31
+ enableAutoDeduct: params.enableAutoDeduct,
32
+ userJolicoin: params.userJoliCoin,
33
+ joliCoinQuantity: params.joliCoinQuantity
34
+ },
35
+ buttons: {
36
+ confirm: {
37
+ text: params.confirmButtonText,
38
+ onPress: () => {
39
+ rewardsEmitter.emit(UseModalResultEventName, { useModalResult: 'CONFIRM' });
40
+ modal.destroy();
41
+ }
42
+ },
43
+ cancel: {
44
+ text: params.cancelButtonText,
45
+ onPress: ({ type }) => {
46
+ rewardsEmitter.emit(UseModalResultEventName, {
47
+ useModalResult: (type ?? '') === 'CANCEL' ? 'CANCEL' : 'FAILED'
48
+ });
49
+ modal.destroy();
50
+ }
51
+ }
52
+ }
53
+ });
54
+ });
55
+
56
+ /**
57
+ * payment jolicoin modal
58
+ */
59
+ rewardsEmitter.on(
60
+ InvokePaymentEventName,
61
+ async (type: 'JOLI_COIN' | 'ADS-JOLI_COIN', params: IInvokePaymentEvent) => {
62
+ try {
63
+ // handle showup frequecy
64
+ if (type === 'ADS-JOLI_COIN') {
65
+ //
66
+ console.log('show by frequency');
67
+ }
68
+
69
+ const balenceDetails = await getBalenceDetails();
70
+ if (!balenceDetails) {
71
+ rewardsEmitter.emit(PaymentResultEventName, { paymentResult: 'FAILED' });
72
+ return;
73
+ }
74
+ const modal = createPaymentJolicoinModal({
75
+ data: {
76
+ userJolicoin: params.userJoliCoin,
77
+ joliCoinQuantity: params.joliCoinQuantity,
78
+ paymentChoices: balenceDetails.paymentChoices,
79
+ enableAutoDeduct: balenceDetails.enableAutoDeduct
80
+ },
81
+ buttons: {
82
+ confirm: {
83
+ text: params.confirmButtonText,
84
+ onPress: async (productId: string) => {
85
+ // TODO: native payment
86
+ // await invokeNativePayment(productId);
87
+ if (!context.hostUserInfo?.isLogin) {
88
+ const { data } = await login();
89
+ if (!data?.isLogin) {
90
+ console.log('login failed');
91
+ return;
92
+ }
93
+ }
94
+ console.log('invokeNativePayment', productId);
95
+ rewardsEmitter.emit(PaymentResultEventName, { paymentResult: 'SUCCESS' });
96
+ modal.destroy();
97
+ }
98
+ },
99
+ cancel: {
100
+ text: params.cancelButtonText,
101
+ onPress: ({ type }) => {
102
+ rewardsEmitter.emit(PaymentResultEventName, {
103
+ paymentResult: (type ?? '') === 'CANCEL' ? 'CANCEL' : 'FAILED'
104
+ });
105
+ modal.destroy();
106
+ }
107
+ }
108
+ }
109
+ });
110
+ } catch (error) {
111
+ console.info('payment failed', error);
112
+ rewardsEmitter.emit(PaymentResultEventName, { paymentResult: 'FAILED' });
113
+ }
114
+ }
115
+ );
116
+
117
+ const getBalenceDetails = async (): Promise<
118
+ | {
119
+ balance: number;
120
+ enableAutoDeduct: boolean;
121
+ paymentChoices: IPaymentChoice[];
122
+ }
123
+ | undefined
124
+ > => {
125
+ const { response } = await fetch<
126
+ StandardResponse<{
127
+ balance: number;
128
+ enableAutoDeduct: boolean;
129
+ paymentChoices: IPaymentChoice[];
130
+ }>
131
+ >('/api/joli-coin/balance-detail', {
132
+ method: 'GET',
133
+ appendHostCookie: true,
134
+ responseType: 'json'
135
+ });
136
+
137
+ return response.data?.data;
138
+ };
@@ -0,0 +1,271 @@
1
+ import { invokeNative } from '@jolibox/native-bridge';
2
+ import { context } from '@/common/context';
3
+ import { hostEmitter } from '@jolibox/common';
4
+
5
+ interface IframeModalOptions {
6
+ url: string;
7
+ height?: string; // default '50%'
8
+ showCloseButton?: boolean; // default true
9
+ enableDragClose?: boolean; // default true
10
+ useAnimation?: boolean; // default true
11
+ onClose?: () => void;
12
+ onLoad?: () => void;
13
+ }
14
+
15
+ const DEFAULT_OPTIONS: Partial<IframeModalOptions> = {
16
+ height: '50%',
17
+ showCloseButton: true,
18
+ enableDragClose: true,
19
+ useAnimation: true
20
+ };
21
+
22
+ let modalOverlay: HTMLElement | null = null;
23
+ let modalContainer: HTMLElement | null = null;
24
+ let iframe: HTMLIFrameElement | null = null;
25
+ /**
26
+ * create a half-screen iframe modal
27
+ */
28
+ export function createIframeModal(options: IframeModalOptions): { close: () => void } {
29
+ const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
30
+ const { url, height, showCloseButton, enableDragClose, useAnimation, onClose, onLoad } = mergedOptions;
31
+
32
+ let startY = 0;
33
+ let currentY = 0;
34
+ let isModalVisible = false;
35
+
36
+ const getAnimationStyles = () => {
37
+ if (useAnimation) {
38
+ return {
39
+ overlayTransition: 'opacity 0.3s ease',
40
+ containerTransition: 'transform 0.3s ease',
41
+ iframeTransition: 'opacity 0.3s ease'
42
+ };
43
+ } else {
44
+ return {
45
+ overlayTransition: 'none',
46
+ containerTransition: 'none',
47
+ iframeTransition: 'none'
48
+ };
49
+ }
50
+ };
51
+
52
+ // 获取动画延迟时间
53
+ const getAnimationDelay = () => (useAnimation ? 300 : 0);
54
+
55
+ // create modal dom
56
+ function createModalDOM() {
57
+ const { overlayTransition, containerTransition, iframeTransition } = getAnimationStyles();
58
+
59
+ modalOverlay = document.createElement('div');
60
+ modalOverlay.id = 'jolibox-modal-overlay';
61
+ modalOverlay.style.cssText = `position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1000; display:flex; justify-content:center; align-items:flex-end; transition:${overlayTransition}; opacity:0; visibility:hidden;`;
62
+
63
+ modalContainer = document.createElement('div');
64
+ modalContainer.id = 'jolibox-modal-container';
65
+ modalContainer.style.cssText = `width:100%; height:${height}; background:white; border-radius:12px 12px 0 0; overflow:hidden; box-shadow:0 -2px 10px rgba(0,0,0,0.2); transform:translateY(100%); transition:${containerTransition};`;
66
+
67
+ const loadingIndicator = document.createElement('div');
68
+ loadingIndicator.id = 'jolibox-modal-loading';
69
+ loadingIndicator.style.cssText =
70
+ 'position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); width:40px; height:40px; border:4px solid #f3f3f3; border-top:4px solid #3498db; border-radius:50%; animation:spin 1s linear infinite;';
71
+
72
+ const styleElement = document.createElement('style');
73
+ styleElement.textContent =
74
+ '@keyframes spin { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } }';
75
+ document.head.appendChild(styleElement);
76
+
77
+ // safe area adapter
78
+ const safeAreaDiv = document.createElement('div');
79
+ safeAreaDiv.style.cssText =
80
+ 'width:100%; height:100%; display:flex; flex-direction:column; padding-bottom:env(safe-area-inset-bottom, 0); position:relative;';
81
+
82
+ // create iframe
83
+ iframe = document.createElement('iframe');
84
+ iframe.style.cssText = `width:100%; flex:1; border:none; opacity:0; transition:${iframeTransition};`;
85
+ iframe.src = url;
86
+
87
+ // iframe loaded event
88
+ iframe.onload = () => {
89
+ if (loadingIndicator.parentNode) {
90
+ loadingIndicator.parentNode.removeChild(loadingIndicator);
91
+ }
92
+
93
+ // 显示iframe内容
94
+ iframe!.style.opacity = '1';
95
+
96
+ // 显示modal
97
+ showModal();
98
+
99
+ // 触发加载完成回调
100
+ onLoad?.();
101
+
102
+ // 发送全局事件
103
+ hostEmitter.emit('onModalIframeLoaded', { url });
104
+ };
105
+
106
+ // 组装DOM
107
+ safeAreaDiv.appendChild(iframe);
108
+ safeAreaDiv.appendChild(loadingIndicator);
109
+ modalContainer.appendChild(safeAreaDiv);
110
+
111
+ // 添加关闭按钮
112
+ if (showCloseButton) {
113
+ const closeBtn = document.createElement('div');
114
+ closeBtn.style.cssText =
115
+ 'position:absolute; top:10px; right:10px; width:30px; height:30px; background:rgba(0,0,0,0.1); border-radius:15px; display:flex; justify-content:center; align-items:center; cursor:pointer; z-index:10;';
116
+ closeBtn.innerHTML = '✕';
117
+ closeBtn.onclick = closeModal;
118
+ modalContainer.appendChild(closeBtn);
119
+ }
120
+
121
+ modalOverlay.appendChild(modalContainer);
122
+ document.body.appendChild(modalOverlay);
123
+
124
+ // 点击遮罩关闭
125
+ modalOverlay.addEventListener('click', function (e) {
126
+ if (e.target === modalOverlay) closeModal();
127
+ });
128
+
129
+ if (enableDragClose) {
130
+ addDragSupport();
131
+ }
132
+ }
133
+
134
+ // show modal
135
+ function showModal() {
136
+ if (!modalOverlay || !modalContainer || isModalVisible) return;
137
+
138
+ modalOverlay.style.visibility = 'visible';
139
+
140
+ if (useAnimation) {
141
+ requestAnimationFrame(() => {
142
+ if (modalOverlay) modalOverlay.style.opacity = '1';
143
+ if (modalContainer) modalContainer.style.transform = 'translateY(0)';
144
+ isModalVisible = true;
145
+ });
146
+ } else {
147
+ if (modalOverlay) modalOverlay.style.opacity = '1';
148
+ if (modalContainer) modalContainer.style.transform = 'translateY(0)';
149
+ isModalVisible = true;
150
+ }
151
+
152
+ invokeNative('updateContainerConfigSync', {
153
+ displayCapsuleButton: false,
154
+ webviewId: context.webviewId
155
+ });
156
+ }
157
+
158
+ function addDragSupport() {
159
+ if (!modalContainer) return;
160
+
161
+ const handleTouchStart = (e: TouchEvent) => {
162
+ startY = e.touches[0].clientY;
163
+ };
164
+
165
+ const handleTouchMove = (e: TouchEvent) => {
166
+ if (!isModalVisible) return;
167
+
168
+ currentY = e.touches[0].clientY;
169
+ const deltaY = currentY - startY;
170
+
171
+ if (deltaY > 0) {
172
+ if (modalContainer) {
173
+ // 拖动时始终不使用过渡效果
174
+ modalContainer.style.transition = 'none';
175
+ modalContainer.style.transform = `translateY(${deltaY}px)`;
176
+ }
177
+ e.preventDefault();
178
+ }
179
+ };
180
+
181
+ const handleTouchEnd = () => {
182
+ if (!isModalVisible || !modalContainer) return;
183
+
184
+ modalContainer.style.transition = useAnimation ? 'transform 0.3s ease' : 'none';
185
+
186
+ if (currentY - startY > 80) {
187
+ closeModal();
188
+ } else {
189
+ modalContainer.style.transform = 'translateY(0)';
190
+ }
191
+ };
192
+
193
+ modalContainer.addEventListener('touchstart', handleTouchStart);
194
+ modalContainer.addEventListener('touchmove', handleTouchMove);
195
+ modalContainer.addEventListener('touchend', handleTouchEnd);
196
+ }
197
+
198
+ // close
199
+ function closeModal() {
200
+ if (!modalOverlay || !modalContainer || !isModalVisible) return;
201
+
202
+ if (useAnimation) {
203
+ modalContainer.style.transform = 'translateY(100%)';
204
+ modalOverlay.style.opacity = '0';
205
+ } else {
206
+ modalOverlay.style.visibility = 'hidden';
207
+ }
208
+
209
+ isModalVisible = false;
210
+
211
+ // 恢复原生UI
212
+ invokeNative('updateContainerConfigSync', {
213
+ displayCapsuleButton: true,
214
+ webviewId: context.webviewId
215
+ });
216
+
217
+ setTimeout(() => {
218
+ if (modalOverlay) {
219
+ modalOverlay.remove();
220
+ modalOverlay = null;
221
+ modalContainer = null;
222
+ iframe = null;
223
+
224
+ // 触发关闭回调
225
+ onClose?.();
226
+
227
+ // 发送全局事件
228
+ hostEmitter.emit('onModalIframeClosed', { url });
229
+ }
230
+ }, getAnimationDelay());
231
+ }
232
+
233
+ createModalDOM();
234
+
235
+ return { close: closeModal };
236
+ }
237
+
238
+ declare global {
239
+ interface Window {
240
+ joliboxUI?: {
241
+ createIframeModal: (options: IframeModalOptions) => { close: () => void };
242
+ };
243
+ }
244
+ }
245
+
246
+ export function registerIframeModalToGlobal() {
247
+ window.joliboxUI = {
248
+ ...window.joliboxUI,
249
+ createIframeModal
250
+ };
251
+
252
+ return () => {
253
+ window.joliboxUI = undefined;
254
+ if (modalOverlay && modalOverlay.parentNode) {
255
+ modalOverlay.parentNode.removeChild(modalOverlay);
256
+ }
257
+
258
+ if (modalContainer && modalContainer.parentNode) {
259
+ modalContainer.parentNode.removeChild(modalContainer);
260
+ }
261
+
262
+ if (iframe && iframe.parentNode) {
263
+ iframe.parentNode.removeChild(iframe);
264
+ }
265
+
266
+ // 清空引用
267
+ modalOverlay = null;
268
+ modalContainer = null;
269
+ iframe = null;
270
+ };
271
+ }