From db8a4d3eb3b95d5b43868733e9280986fb957a15 Mon Sep 17 00:00:00 2001 From: Jay Date: Sat, 19 Mar 2022 23:39:49 +0900 Subject: [PATCH 01/12] feat: add full screen ad hooks --- example/yarn.lock | 13 +++++ package.json | 3 + src/hooks/useAppOpenAd.ts | 47 +++++++++++++++ src/hooks/useFullScreenAd.ts | 104 +++++++++++++++++++++++++++++++++ src/hooks/useInterstitialAd.ts | 47 +++++++++++++++ src/hooks/useRewardedAd.ts | 47 +++++++++++++++ src/index.ts | 3 + src/types/AdStates.ts | 51 ++++++++++++++++ yarn.lock | 20 +++++++ 9 files changed, 335 insertions(+) create mode 100644 src/hooks/useAppOpenAd.ts create mode 100644 src/hooks/useFullScreenAd.ts create mode 100644 src/hooks/useInterstitialAd.ts create mode 100644 src/hooks/useRewardedAd.ts create mode 100644 src/types/AdStates.ts diff --git a/example/yarn.lock b/example/yarn.lock index de63b39e..d321ccfa 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -2869,6 +2869,11 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +dequal@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d" + integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -7631,6 +7636,14 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +use-deep-compare-effect@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/use-deep-compare-effect/-/use-deep-compare-effect-1.8.1.tgz#ef0ce3b3271edb801da1ec23bf0754ef4189d0c6" + integrity sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q== + dependencies: + "@babel/runtime" "^7.12.5" + dequal "^2.0.2" + use-subscription@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" diff --git a/package.json b/package.json index 869473fe..10cbbb4e 100644 --- a/package.json +++ b/package.json @@ -141,5 +141,8 @@ }, "publishConfig": { "access": "public" + }, + "dependencies": { + "use-deep-compare-effect": "^1.8.1" } } diff --git a/src/hooks/useAppOpenAd.ts b/src/hooks/useAppOpenAd.ts new file mode 100644 index 00000000..466f60bc --- /dev/null +++ b/src/hooks/useAppOpenAd.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { useState } from 'react'; +import useDeepCompareEffect from 'use-deep-compare-effect'; +import { AppOpenAd } from '../ads/AppOpenAd'; + +import { AdHookReturns } from '../types/AdStates'; +import { RequestOptions } from '../types/RequestOptions'; + +import { useFullScreenAd } from './useFullScreenAd'; + +/** + * React Hook for App Open Ad. + * + * @param adUnitId The Ad Unit ID for the App Open Ad. You can find this on your Google Mobile Ads dashboard. + * @param requestOptions Optional RequestOptions used to load the ad. + */ +export function useAppOpenAd( + adUnitId: string | null, + requestOptions?: RequestOptions, +): Omit { + const [appOpenAd, setAppOpenAd] = useState(null); + + useDeepCompareEffect(() => { + setAppOpenAd(() => { + //prevAd?.destroy(); + return adUnitId ? AppOpenAd.createForAdRequest(adUnitId, requestOptions) : null; + }); + }, [adUnitId, requestOptions]); + + return useFullScreenAd(appOpenAd); +} diff --git a/src/hooks/useFullScreenAd.ts b/src/hooks/useFullScreenAd.ts new file mode 100644 index 00000000..e5495814 --- /dev/null +++ b/src/hooks/useFullScreenAd.ts @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Reducer, useCallback, useEffect, useReducer } from 'react'; +import { AdEventType } from '../AdEventType'; + +import { AppOpenAd } from '../ads/AppOpenAd'; +import { InterstitialAd } from '../ads/InterstitialAd'; +import { RewardedAd } from '../ads/RewardedAd'; +import { RewardedAdEventType } from '../RewardedAdEventType'; +import { AdStates, AdHookReturns } from '../types/AdStates'; +import { AdShowOptions } from '../types/AdShowOptions'; + +const initialState: AdStates = { + isLoaded: false, + isOpened: false, + isClicked: false, + isClosed: false, + error: undefined, + reward: undefined, + isEarnedReward: false, +}; + +export function useFullScreenAd( + ad: T, +): AdHookReturns { + const [state, setState] = useReducer>>( + (prevState, newState) => ({ ...prevState, ...newState }), + initialState, + ); + const isShowing = state.isOpened && !state.isClosed; + + const load = useCallback(() => { + if (ad) { + setState(initialState); + ad.load(); + } + }, [ad]); + + const show = useCallback( + (showOptions?: AdShowOptions) => { + if (ad) { + ad.show(showOptions); + } + }, + [ad], + ); + + useEffect(() => { + setState(initialState); + if (!ad) { + return; + } + const unsubscribe = ad.onAdEvent((type, error, data) => { + switch (type) { + case AdEventType.LOADED: + setState({ isLoaded: true }); + break; + case AdEventType.OPENED: + setState({ isOpened: true }); + break; + case AdEventType.CLOSED: + setState({ isClosed: true }); + break; + case AdEventType.CLICKED: + setState({ isClicked: true }); + break; + case AdEventType.ERROR: + setState({ error: error }); + break; + case RewardedAdEventType.LOADED: + setState({ isLoaded: true, reward: data }); + break; + case RewardedAdEventType.EARNED_REWARD: + setState({ isEarnedReward: true, reward: data }); + break; + } + }); + return () => { + unsubscribe(); + }; + }, [ad]); + + return { + ...state, + isShowing, + load, + show, + }; +} diff --git a/src/hooks/useInterstitialAd.ts b/src/hooks/useInterstitialAd.ts new file mode 100644 index 00000000..f9a01e9a --- /dev/null +++ b/src/hooks/useInterstitialAd.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { useState } from 'react'; +import useDeepCompareEffect from 'use-deep-compare-effect'; + +import { InterstitialAd } from '../ads/InterstitialAd'; +import { AdHookReturns } from '../types/AdStates'; +import { RequestOptions } from '../types/RequestOptions'; + +import { useFullScreenAd } from './useFullScreenAd'; + +/** + * React Hook for Interstitial Ad. + * + * @param adUnitId The Ad Unit ID for the Interstitial Ad. You can find this on your Google Mobile Ads dashboard. + * @param requestOptions Optional RequestOptions used to load the ad. + */ +export function useInterstitialAd( + adUnitId: string | null, + requestOptions?: RequestOptions, +): Omit { + const [interstitialAd, setInterstitialAd] = useState(null); + + useDeepCompareEffect(() => { + setInterstitialAd(() => { + //prevAd?.destroy(); + return adUnitId ? InterstitialAd.createForAdRequest(adUnitId, requestOptions) : null; + }); + }, [adUnitId, requestOptions]); + + return useFullScreenAd(interstitialAd); +} diff --git a/src/hooks/useRewardedAd.ts b/src/hooks/useRewardedAd.ts new file mode 100644 index 00000000..6d97aeac --- /dev/null +++ b/src/hooks/useRewardedAd.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { useState } from 'react'; +import useDeepCompareEffect from 'use-deep-compare-effect'; + +import { RewardedAd } from '../ads/RewardedAd'; +import { AdHookReturns } from '../types/AdStates'; +import { RequestOptions } from '../types/RequestOptions'; + +import { useFullScreenAd } from './useFullScreenAd'; + +/** + * React Hook for Rewarded Ad. + * + * @param adUnitId The Ad Unit ID for the Rewarded Ad. You can find this on your Google Mobile Ads dashboard. + * @param requestOptions Optional RequestOptions used to load the ad. + */ +export function useRewardedAd( + adUnitId: string | null, + requestOptions?: RequestOptions, +): Omit { + const [rewardedAd, setRewardedAd] = useState(null); + + useDeepCompareEffect(() => { + setRewardedAd(() => { + //prevAd?.destroy(); + return adUnitId ? RewardedAd.createForAdRequest(adUnitId, requestOptions) : null; + }); + }, [adUnitId, requestOptions]); + + return useFullScreenAd(rewardedAd); +} diff --git a/src/index.ts b/src/index.ts index 671f090a..e8d7bb24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,3 +33,6 @@ export { AppOpenAd } from './ads/AppOpenAd'; export { InterstitialAd } from './ads/InterstitialAd'; export { RewardedAd } from './ads/RewardedAd'; export { BannerAd } from './ads/BannerAd'; +export { useAppOpenAd } from './hooks/useAppOpenAd'; +export { useInterstitialAd } from './hooks/useInterstitialAd'; +export { useRewardedAd } from './hooks/useRewardedAd'; diff --git a/src/types/AdStates.ts b/src/types/AdStates.ts new file mode 100644 index 00000000..671f7811 --- /dev/null +++ b/src/types/AdStates.ts @@ -0,0 +1,51 @@ +import { AdShowOptions } from './AdShowOptions'; +import { RewardedAdReward } from './RewardedAdReward'; + +export interface AdStates { + /** + * Whether the ad is loaded and ready to to be shown to the user. + */ + isLoaded: boolean; + /** + * Whether the ad is opened. + */ + isOpened: boolean; + /** + * Whether the user clicked the advert. + */ + isClicked: boolean; + /** + * Whether the user closed the ad and has returned back to your application. + */ + isClosed: boolean; + /** + * JavaScript Error containing the error code and message thrown by the Ad. + */ + error?: Error; + /** + * Loaded reward item of the Rewarded Ad. + */ + reward?: RewardedAdReward; + /** + * Whether the user earned the reward by Rewarded Ad. + */ + isEarnedReward?: boolean; +} + +export interface AdHookReturns extends AdStates { + /** + * Whether your ad is showing. + * The value is equal with `isOpened && !isClosed`. + */ + isShowing: boolean; + /** + * Start loading the advert with the provided RequestOptions. + */ + load: () => void; + /** + * Show the loaded advert to the user. + * + * @param showOptions An optional `AdShowOptions` interface. + */ + show: (showOptions?: AdShowOptions) => void; +} diff --git a/yarn.lock b/yarn.lock index cfbfef81..99261f61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1654,6 +1654,13 @@ pirates "^4.0.0" source-map-support "^0.5.16" +"@babel/runtime@^7.12.5": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" + integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.8.4": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" @@ -5155,6 +5162,11 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +dequal@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d" + integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -11843,6 +11855,14 @@ urlgrey@1.0.0: dependencies: fast-url-parser "^1.1.3" +use-deep-compare-effect@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/use-deep-compare-effect/-/use-deep-compare-effect-1.8.1.tgz#ef0ce3b3271edb801da1ec23bf0754ef4189d0c6" + integrity sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q== + dependencies: + "@babel/runtime" "^7.12.5" + dequal "^2.0.2" + use-subscription@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" From 564e17a523a6340eea072471bb62d461da04a038 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 20 Mar 2022 16:39:29 +0900 Subject: [PATCH 02/12] fix: set empty object as default requestOptions useDeepCompareEffect doesn't accept primitive value only as its dependency array. To avoid that, pass empty object as requestOptions instead of undefined value. --- src/hooks/useAppOpenAd.ts | 2 +- src/hooks/useInterstitialAd.ts | 2 +- src/hooks/useRewardedAd.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useAppOpenAd.ts b/src/hooks/useAppOpenAd.ts index 466f60bc..336b6904 100644 --- a/src/hooks/useAppOpenAd.ts +++ b/src/hooks/useAppOpenAd.ts @@ -32,7 +32,7 @@ import { useFullScreenAd } from './useFullScreenAd'; */ export function useAppOpenAd( adUnitId: string | null, - requestOptions?: RequestOptions, + requestOptions: RequestOptions = {}, ): Omit { const [appOpenAd, setAppOpenAd] = useState(null); diff --git a/src/hooks/useInterstitialAd.ts b/src/hooks/useInterstitialAd.ts index f9a01e9a..6ffe0148 100644 --- a/src/hooks/useInterstitialAd.ts +++ b/src/hooks/useInterstitialAd.ts @@ -32,7 +32,7 @@ import { useFullScreenAd } from './useFullScreenAd'; */ export function useInterstitialAd( adUnitId: string | null, - requestOptions?: RequestOptions, + requestOptions: RequestOptions = {}, ): Omit { const [interstitialAd, setInterstitialAd] = useState(null); diff --git a/src/hooks/useRewardedAd.ts b/src/hooks/useRewardedAd.ts index 6d97aeac..bf7e960a 100644 --- a/src/hooks/useRewardedAd.ts +++ b/src/hooks/useRewardedAd.ts @@ -32,7 +32,7 @@ import { useFullScreenAd } from './useFullScreenAd'; */ export function useRewardedAd( adUnitId: string | null, - requestOptions?: RequestOptions, + requestOptions: RequestOptions = {}, ): Omit { const [rewardedAd, setRewardedAd] = useState(null); From 3c470a4a9b4de3e376bfb15f425557316340fd63 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 20 Mar 2022 16:46:46 +0900 Subject: [PATCH 03/12] docs: add documentation for using hooks --- docs.json | 3 +- docs/displaying-ads-hook.mdx | 99 ++++++++++++++++++++++++++++++++++ src/hooks/useAppOpenAd.ts | 4 +- src/hooks/useFullScreenAd.ts | 2 +- src/hooks/useInterstitialAd.ts | 2 +- src/hooks/useRewardedAd.ts | 2 +- src/types/AdStates.ts | 36 +++++++++++++ 7 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 docs/displaying-ads-hook.mdx diff --git a/docs.json b/docs.json index a4e9f1fa..3ea10adc 100644 --- a/docs.json +++ b/docs.json @@ -4,8 +4,9 @@ "sidebar": [ ["Installation", "/"], ["Displaying Ads", "/displaying-ads"], + ["Displaying Ads via Hook", "/displaying-ads-hook"], ["European User Consent", "/european-user-consent"], ["Common Reasons For Ads Not Showing", "/common-reasons-for-ads-not-showing"], ["Migrating to v5", "/migrating-to-v5"] ] -} +} diff --git a/docs/displaying-ads-hook.mdx b/docs/displaying-ads-hook.mdx new file mode 100644 index 00000000..f83ec7a2 --- /dev/null +++ b/docs/displaying-ads-hook.mdx @@ -0,0 +1,99 @@ +# Hooks + +The AdMob package provides hooks to help you to display ads in functional component with tiny code. The supported ad formats are full-screen-type-ads: AppOpen, Interstitial, Rewarded. + +## Creating ad + +You can create a new ad by adding a corresponding ad type's hook to your component. + +The first argument of the hook is the "Ad Unit ID". +For testing, we can use a Test ID, however for production the ID from the +Google AdMob dashboard under "Ad units" should be used: + +```tsx {4-8} +import { useInterstitialAd, TestIds } from 'react-native-google-mobile-ads'; + +export default function App() { + const interstitialAd = useInterstitialAd(TestIds.Interstitial, { + requestNonPersonalizedAdsOnly: true, + }); + + return {/* ... */}; +} +``` + +> `adUnitid` parameter is also can be used to manage creation and destruction of ad instance. +> If `adUnitid` is set or changed, new ad instance will be created and previous ad instance will be destroyed if exists. +> If `adUnitid` is set to `null`, no ad instance will be created and previous ad instance will be destroyed if exists. + +The second argument is additional optional request options object to be sent whilst loading an advert, such as keywords & location. +Setting additional request options helps AdMob choose better tailored ads from the network. View the [`RequestOptions`](/reference/admob/requestoptions) +documentation to view the full range of options available. + +## Displaying ad + +The hook returns several states and functions to control ad. + +```tsx +import { useInterstitialAd, TestIds } from 'react-native-google-mobile-ads'; + +export default function App({ navigation }) { + const { isLoaded, isClosed, load, show } = useInterstitialAd(TestIds.Interstitial, { + requestNonPersonalizedAdsOnly: true, + }); + + useEffect(() => { + // Start loading the interstitial straight away + load(); + }, [load]); + + useEffect(() => { + if (isClosed) { + // Action after the ad is closed + navigation.navigate('NextScreen'); + } + }, [isClosed, navigation]); + + return ( + +