Skip to content

Commit

Permalink
feat: hooks for all full screen ad types (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
wjaykim committed Apr 8, 2022
1 parent df2fc26 commit 0bd7ce8
Show file tree
Hide file tree
Showing 13 changed files with 516 additions and 4 deletions.
3 changes: 2 additions & 1 deletion docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
]
}
}
99 changes: 99 additions & 0 deletions docs/displaying-ads-hook.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Hooks

The AdMob package provides hooks to help you to display ads in a functional component with tiny code. The supported ad formats are full-screen ads: App open, Interstitial & Rewarded.

## Load an 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 <View>{/* ... */}</View>;
}
```

> The `adUnitid` parameter can also be used to manage creation and destruction of an 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 an 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.

## Show the 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 (
<View>
<Button
title="Navigate to next screen"
onPress={() => {
if (isLoaded) {
show();
} else {
// No advert ready to show yet
navigation.navigate('NextScreen');
}
}}
/>
</View>
);
}
```

The code above immediately starts to load a new advert from the network (via `load()`).
When user presses button, it checks if the ad is loaded via `isLoaded` value,
then the `show` function is called and the advert is shown over-the-top of your application.
Otherwise, if the ad is not loaded, the `navigation.navigate` method is called to navigate to the next screen without showing the ad.
After the ad is closed, the `isClosed` value is set to `true` and the `navigation.navigate` method is called to navigate to the next screen.

If needed, you can reuse the existing hook to load more adverts and show them when required. The states are initialized when the `load` function is called.

Return values of the hook are:

| Name | Type | Description |
| :------------- | :--------------------------------- | :----------------------------------------------------------------------------------------------------------------- |
| isLoaded | boolean | Whether the ad is loaded and ready to to be shown to the user. |
| isOpened | boolean | Whether the ad is opened. The value is remained `true` even after the ad is closed unless **new ad is requested**. |
| isClosed | boolean | Whether your ad is dismissed. |
| isShowing | boolean | Whether your ad is showing. The value is equal with `isOpened && !isClosed`. |
| error | Error \| undefined | `Error` object throwed during ad load. |
| reward | [RewardedAdReward](#) \| undefined | Loaded reward item of the Rewarded Ad. Available only in RewardedAd. |
| isEarnedReward | boolean | Whether the user earned the reward by Rewarded Ad. |
| load | Function | Start loading the advert with the provided RequestOptions. |
| show | Function | Show the loaded advert to the user. |

> Note that `isOpened` value remains `true` even after the ad is closed.
> The value changes to `false` when ad is initialized via calling `load()`.
> To determine whether the ad is currently showing, use `isShowing` value.
46 changes: 45 additions & 1 deletion example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useEffect} from 'react';
import {
Button,
SafeAreaView,
Expand All @@ -19,6 +19,7 @@ import {
BannerAd,
BannerAdSize,
RewardedAd,
useInterstitialAd,
} from 'react-native-google-mobile-ads';

const appOpenAdUnitId = TestIds.APP_OPEN;
Expand Down Expand Up @@ -244,12 +245,55 @@ class AdConsentTest implements Test {
}
}

const HookComponent = React.forwardRef<View>((_, ref) => {
const {load, show} = useInterstitialAd(TestIds.INTERSTITIAL);
useEffect(() => {
load();
}, [load]);
return (
<View style={styles.testSpacing} ref={ref}>
<Button
title="Show Interstitial"
onPress={() => {
show();
}}
/>
</View>
);
});

class HookTest implements Test {
getPath(): string {
return 'Hook';
}

getTestType(): TestType {
return TestType.Interactive;
}

render(onMount: (component: any) => void): React.ReactNode {
return <HookComponent ref={onMount} />;
}

execute(component: any, complete: (result: TestResult) => void): void {
let results = new TestResult();
try {
// You can do anything here, it will execute on-device + in-app. Results are aggregated + visible in-app.
} catch (error) {
results.errors.push('Received unexpected error...');
} finally {
complete(results);
}
}
}

// All tests must be registered - a future feature will allow auto-bundling of tests via configured path or regex
TestRegistry.registerTest(new BannerTest());
TestRegistry.registerTest(new AppOpenTest());
TestRegistry.registerTest(new InterstitialTest());
TestRegistry.registerTest(new RewaredTest());
TestRegistry.registerTest(new AdConsentTest());
TestRegistry.registerTest(new HookTest());

const App = () => {
return (
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ PODS:
- React-jsi (= 0.67.3)
- React-logger (= 0.67.3)
- React-perflogger (= 0.67.3)
- RNGoogleMobileAds (4.2.0):
- RNGoogleMobileAds (5.0.0):
- Google-Mobile-Ads-SDK (= 8.13.0)
- GoogleUserMessagingPlatform (= 2.0.0)
- React-Core
Expand Down Expand Up @@ -591,7 +591,7 @@ SPEC CHECKSUMS:
React-RCTVibration: d0361f15ea978958fab7ffb6960f475b5063d83f
React-runtimeexecutor: af1946623656f9c5fd64ca6f36f3863516193446
ReactCommon: 650e33cde4fb7d36781cd3143f5276da0abb2f96
RNGoogleMobileAds: ae52d9f8048e7a5f5139b9440fec5a839175b9a5
RNGoogleMobileAds: 3b35145daceeb09105dcafa4778d65678486a53a
Yoga: 90dcd029e45d8a7c1ff059e8b3c6612ff409061a
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

Expand Down
13 changes: 13 additions & 0 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,5 +141,8 @@
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"use-deep-compare-effect": "^1.8.1"
}
}
46 changes: 46 additions & 0 deletions src/hooks/useAppOpenAd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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. You can destroy ad instance by setting this value to null.
* @param requestOptions Optional RequestOptions used to load the ad.
*/
export function useAppOpenAd(
adUnitId: string | null,
requestOptions: RequestOptions = {},
): Omit<AdHookReturns, 'adReward'> {
const [appOpenAd, setAppOpenAd] = useState<AppOpenAd | null>(null);

useDeepCompareEffect(() => {
setAppOpenAd(() => {
return adUnitId ? AppOpenAd.createForAdRequest(adUnitId, requestOptions) : null;
});
}, [adUnitId, requestOptions]);

return useFullScreenAd(appOpenAd);
}
104 changes: 104 additions & 0 deletions src/hooks/useFullScreenAd.ts
Original file line number Diff line number Diff line change
@@ -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<T extends InterstitialAd | RewardedAd | AppOpenAd | null>(
ad: T,
): AdHookReturns {
const [state, setState] = useReducer<Reducer<AdStates, Partial<AdStates>>>(
(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,
};
}

0 comments on commit 0bd7ce8

Please sign in to comment.