ユーザーによる紹介に報酬を付与して新規ユーザーを獲得する

新しいユーザーを獲得する方法として特に効果があるものの 1 つとして、ユーザーによる紹介が挙げられます。Realtime DatabaseCloud Functions for Firebase で Dynamic Links を使用して、紹介者と紹介された受信者の両方がアプリ内報酬を獲得できるようにすることで、ユーザーによる友だちの招待を促すことができます。

主なメリット

  • 友だちを招待してくれたユーザーにインセンティブを提供することで、拡大を促します。
  • 招待リンクは複数のプラットフォームで動作します。
  • 新しいユーザーが初めてアプリを開いたときのアプリの初期動作を、ユーザー用にカスタマイズできます。たとえば、招待した友人と自動的につながるようにすることなどができます。
  • 新しいユーザーがチュートリアルの完了などの入門的なタスクを完了するまで、報酬の付与を延期することもできます。

この機能を導入する手順は以下のとおりです。

Firebase と Dynamic Links SDK を設定する

新しい Firebase プロジェクトを設定し、アプリに Dynamic Links SDK をインストールします(iOSAndroidC++Unity)。Dynamic Links SDK をインストールすると、ダイナミック リンクに関するデータを Firebase からアプリに渡せるようになります(ユーザーがアプリをインストールした後も含む)。この SDK を使用する以外に、アプリをインストール済みのユーザーがアプリをインストールしていないユーザーにクリック操作で接続する方法はありません。

招待状を作成するには、まず招待を受け入れるために受信者が開くリンクを作成します。このリンクは、後で招待状のテキストに含めます。招待状の受信者がリンクを開いてアプリをインストールすると、アプリ内報酬を受け取るなど、初めてアプリを開いたときのアプリの初期動作をカスタマイズできます。

この招待リンクは、既存のユーザーからの招待であることを示す link パラメータ値を持つダイナミック リンクです。

これらの link パラメータのペイロードをフォーマットしてアプリに結び付ける方法は多数あります。たとえば、次の例のようにクエリ パラメータで送信者のユーザー アカウント ID を指定する方法が挙げられます。

https://mygame.example.com/?invitedby=SENDER_UID

また、招待状に含めるのに適したダイナミック リンクを作成するには、次のように Dynamic Link Builder API を使用します。

Swift

注: この Firebase プロダクトは、macOS、Mac Catalyst、tvOS、watchOS の各ターゲットでは使用できません。
guard let uid = Auth.auth().currentUser?.uid else { return }
let link = URL(string: "https://mygame.example.com/?invitedby=\(uid)")
let referralLink = DynamicLinkComponents(link: link!, domain: "example.page.link")

referralLink.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.example.ios")
referralLink.iOSParameters?.minimumAppVersion = "1.0.1"
referralLink.iOSParameters?.appStoreID = "123456789"

referralLink.androidParameters = DynamicLinkAndroidParameters(packageName: "com.example.android")
referralLink.androidParameters?.minimumVersion = 125

referralLink.shorten { (shortURL, warnings, error) in
  if let error = error {
    print(error.localizedDescription)
    return
  }
  self.invitationUrl = shortURL
}

Kotlin+KTX

val user = Firebase.auth.currentUser!!
val uid = user.uid
val invitationLink = "https://mygame.example.com/?invitedby=$uid"
Firebase.dynamicLinks.shortLinkAsync {
    link = Uri.parse(invitationLink)
    domainUriPrefix = "https://example.page.link"
    androidParameters("com.example.android") {
        minimumVersion = 125
    }
    iosParameters("com.example.ios") {
        appStoreId = "123456789"
        minimumVersion = "1.0.1"
    }
}.addOnSuccessListener { shortDynamicLink ->
    mInvitationUrl = shortDynamicLink.shortLink
    // ...
}

Java

FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
String uid = user.getUid();
String link = "https://mygame.example.com/?invitedby=" + uid;
FirebaseDynamicLinks.getInstance().createDynamicLink()
        .setLink(Uri.parse(link))
        .setDomainUriPrefix("https://example.page.link")
        .setAndroidParameters(
                new DynamicLink.AndroidParameters.Builder("com.example.android")
                        .setMinimumVersion(125)
                        .build())
        .setIosParameters(
                new DynamicLink.IosParameters.Builder("com.example.ios")
                        .setAppStoreId("123456789")
                        .setMinimumVersion("1.0.1")
                        .build())
        .buildShortDynamicLink()
        .addOnSuccessListener(new OnSuccessListener<ShortDynamicLink>() {
            @Override
            public void onSuccess(ShortDynamicLink shortDynamicLink) {
                mInvitationUrl = shortDynamicLink.getShortLink();
                // ...
            }
        });

招待状を送信する

リンクを作成したら、それを含んだ招待状を作成します。招待状の形式は、自分のアプリや対象ユーザーにより、メールや SMS メッセージなどのメディアから適当なものを選ぶことができます。

たとえば、メールの招待状を送信するには、次のように記述します。

Swift

注: この Firebase プロダクトは、macOS、Mac Catalyst、tvOS、watchOS の各ターゲットでは使用できません。
guard let referrerName = Auth.auth().currentUser?.displayName else { return }
let subject = "\(referrerName) wants you to play MyExampleGame!"
let invitationLink = invitationUrl?.absoluteString
let msg = "<p>Let's play MyExampleGame together! Use my <a href=\"\(invitationLink)\">referrer link</a>!</p>"

if !MFMailComposeViewController.canSendMail() {
  // Device can't send email
  return
}
let mailer = MFMailComposeViewController()
mailer.mailComposeDelegate = self
mailer.setSubject(subject)
mailer.setMessageBody(msg, isHTML: true)
myView.present(mailer, animated: true, completion: nil)

Kotlin+KTX

val referrerName = Firebase.auth.currentUser?.displayName
val subject = String.format("%s wants you to play MyExampleGame!", referrerName)
val invitationLink = mInvitationUrl.toString()
val msg = "Let's play MyExampleGame together! Use my referrer link: $invitationLink"
val msgHtml = String.format("<p>Let's play MyExampleGame together! Use my " +
        "<a href=\"%s\">referrer link</a>!</p>", invitationLink)

val intent = Intent(Intent.ACTION_SENDTO).apply {
    data = Uri.parse("mailto:") // only email apps should handle this
    putExtra(Intent.EXTRA_SUBJECT, subject)
    putExtra(Intent.EXTRA_TEXT, msg)
    putExtra(Intent.EXTRA_HTML_TEXT, msgHtml)
}
intent.resolveActivity(packageManager)?.let {
    startActivity(intent)
}

Java

String referrerName = FirebaseAuth.getInstance().getCurrentUser().getDisplayName();
String subject = String.format("%s wants you to play MyExampleGame!", referrerName);
String invitationLink = mInvitationUrl.toString();
String msg = "Let's play MyExampleGame together! Use my referrer link: "
        + invitationLink;
String msgHtml = String.format("<p>Let's play MyExampleGame together! Use my "
        + "<a href=\"%s\">referrer link</a>!</p>", invitationLink);

Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:")); // only email apps should handle this
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, msg);
intent.putExtra(Intent.EXTRA_HTML_TEXT, msgHtml);
if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(intent);
}

アプリで紹介情報を取得する

招待状の受信者が紹介リンクを開いた際、まだアプリがインストールされていない場合はアプリをインストールするよう App Store または Play ストアに誘導されます。受信者がインストール後に初めてアプリを開くと、ダイナミック リンクに含めた紹介情報をアプリ側で取得できます。この情報を使って報酬を付与できます。

紹介報酬を付与するタイミングは、招待状の受信者が登録をした後や、なんらかのタスクを完了した後などに設定するのが一般的です。そのため、報酬を付与する基準が満たされるまで、ダイナミック リンクから得られる情報を追跡する必要があります。

この情報を追跡する方法のひとつとして、受信者を匿名ユーザーとしてログインさせ、匿名アカウントの Realtime Database レコードにデータを保存する方法が挙げられます。受信者が登録を完了し、匿名アカウントが永久アカウントに変換されると、新しいアカウントには匿名アカウントと同じ UID が割り当てられ、報酬情報にアクセスできるようになります。

たとえば、受信者がアプリを開いた後に紹介者の UID を保存するには、次のように記述します。

Swift

注: この Firebase プロダクトは、macOS、Mac Catalyst、tvOS、watchOS の各ターゲットでは使用できません。
struct MyApplication: App {

  var body: some Scene {
    WindowGroup {
      VStack {
        Text("Example text")
      }
      .onOpenURL { url in
        if DynamicLinks.dynamicLinks()?.shouldHandleDynamicLink(fromCustomSchemeURL: url) ?? false {
        let dynamicLink = DynamicLinks.dynamicLinks()?.dynamicLink(fromCustomSchemeURL: url)
        handleDynamicLink(dynamicLink)
      }
      // Handle incoming URL with other methods as necessary
      // ...
      }
    }
  }
}

func handleDynamicLink(_ dynamicLink: DynamicLink?) {
  guard let dynamicLink = dynamicLink else { return false }
  guard let deepLink = dynamicLink.url else { return false }
  let queryItems = URLComponents(url: deepLink, resolvingAgainstBaseURL: true)?.queryItems
  let invitedBy = queryItems?.filter({(item) in item.name == "invitedby"}).first?.value
  let user = Auth.auth().currentUser
  // If the user isn't signed in and the app was opened via an invitation
  // link, sign in the user anonymously and record the referrer UID in the
  // user's RTDB record.
  if user == nil && invitedBy != nil {
    Auth.auth().signInAnonymously() { (user, error) in
      if let user = user {
        let userRecord = Database.database().reference().child("users").child(user.uid)
        userRecord.child("referred_by").setValue(invitedBy)
        if dynamicLink.matchConfidence == .weak {
          // If the Dynamic Link has a weak match confidence, it is possible
          // that the current device isn't the same device on which the invitation
          // link was originally opened. The way you handle this situation
          // depends on your app, but in general, you should avoid exposing
          // personal information, such as the referrer's email address, to
          // the user.
        }
      }
    }
  }
}

Kotlin+KTX

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // ...

    Firebase.dynamicLinks
            .getDynamicLink(intent)
            .addOnSuccessListener(this) { pendingDynamicLinkData ->
                // Get deep link from result (may be null if no link is found)
                var deepLink: Uri? = null
                if (pendingDynamicLinkData != null) {
                    deepLink = pendingDynamicLinkData.link
                }
                //
                // If the user isn't signed in and the pending Dynamic Link is
                // an invitation, sign in the user anonymously, and record the
                // referrer's UID.
                //
                val user = Firebase.auth.currentUser
                if (user == null &&
                        deepLink != null &&
                        deepLink.getBooleanQueryParameter("invitedby", false)) {
                    val referrerUid = deepLink.getQueryParameter("invitedby")
                    createAnonymousAccountWithReferrerInfo(referrerUid)
                }
            }
}

private fun createAnonymousAccountWithReferrerInfo(referrerUid: String?) {
    Firebase.auth
            .signInAnonymously()
            .addOnSuccessListener {
                // Keep track of the referrer in the RTDB. Database calls
                // will depend on the structure of your app's RTDB.
                val user = Firebase.auth.currentUser
                val userRecord = Firebase.database.reference
                        .child("users")
                        .child(user!!.uid)
                userRecord.child("referred_by").setValue(referrerUid)
            }
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // ...

    FirebaseDynamicLinks.getInstance()
            .getDynamicLink(getIntent())
            .addOnSuccessListener(this, new OnSuccessListener<PendingDynamicLinkData>() {
                @Override
                public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) {
                    // Get deep link from result (may be null if no link is found)
                    Uri deepLink = null;
                    if (pendingDynamicLinkData != null) {
                        deepLink = pendingDynamicLinkData.getLink();
                    }
                    //
                    // If the user isn't signed in and the pending Dynamic Link is
                    // an invitation, sign in the user anonymously, and record the
                    // referrer's UID.
                    //
                    FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
                    if (user == null
                            && deepLink != null
                            && deepLink.getBooleanQueryParameter("invitedby", false)) {
                        String referrerUid = deepLink.getQueryParameter("invitedby");
                        createAnonymousAccountWithReferrerInfo(referrerUid);
                    }
                }
            });
}

private void createAnonymousAccountWithReferrerInfo(final String referrerUid) {
    FirebaseAuth.getInstance()
            .signInAnonymously()
            .addOnSuccessListener(new OnSuccessListener<AuthResult>() {
                @Override
                public void onSuccess(AuthResult authResult) {
                    // Keep track of the referrer in the RTDB. Database calls
                    // will depend on the structure of your app's RTDB.
                    FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
                    DatabaseReference userRecord =
                            FirebaseDatabase.getInstance().getReference()
                                    .child("users")
                                    .child(user.getUid());
                    userRecord.child("referred_by").setValue(referrerUid);
                }
            });
}

続いて招待状の受信者がアカウントを作成することにした際に、匿名アカウントから招待状の受信者の新しいアカウントに紹介情報を引き継ぐことができます。

まず、招待者が希望するログイン方法を使用して AuthCredential オブジェクトを取得します。たとえば、メールアドレスとパスワードでログインする場合は、次のように記述します。

Swift

注: この Firebase プロダクトは、macOS、Mac Catalyst、tvOS、watchOS の各ターゲットでは使用できません。
let credential = EmailAuthProvider.credential(withEmail: email, password: password)

Kotlin+KTX

val credential = EmailAuthProvider.getCredential(email, password)

Java

AuthCredential credential = EmailAuthProvider.getCredential(email, password);

次に、この認証情報を匿名アカウントにリンクします。

Swift

注: この Firebase プロダクトは、macOS、Mac Catalyst、tvOS、watchOS の各ターゲットでは使用できません。
if let user = Auth.auth().currentUser {
  user.link(with: credential) { (user, error) in
    // Complete any post sign-up tasks here.
  }
}

Kotlin+KTX

Firebase.auth.currentUser!!
        .linkWithCredential(credential)
        .addOnSuccessListener {
            // Complete any post sign-up tasks here.
        }

Java

FirebaseAuth.getInstance().getCurrentUser()
        .linkWithCredential(credential)
        .addOnSuccessListener(new OnSuccessListener<AuthResult>() {
            @Override
            public void onSuccess(AuthResult authResult) {
                // Complete any post sign-up tasks here.
            }
        });

これによって、新しい永久アカウントが匿名アカウントに追加されたすべての報酬データにアクセスできるようになります。

紹介者と受信者に報酬を与える

ここまでの作業でダイナミック リンクから招待データを取得して保存しているため、必要な基準が満たされたときに紹介者と受信者に紹介報酬を付与することができます。

クライアント アプリから Realtime Database に書き込むことは可能ですが、一般的にアプリからはアプリ内通貨などのデータへの読み取りアクセスのみを許可し、バックエンドからは書き込みオペレーションのみを許可するようにします。このバックエンドは Firebase Admin SDK を実行できるシステムであれば何でもかまいませんが、通常は Cloud Functions を使用してこれらのタスクを実行するのが最も簡単な方法です。

たとえば、ゲームを配信していて、受信者には登録後にゲーム内の通貨で報酬を付与し、紹介者には受信者がレベル 5 に達した時点で報酬を付与するとします。

登録報酬を付与するには、特定の Realtime Database キーが作成されるのを監視する関数をデプロイし、作成された場合に報酬を付与します。例:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

exports.grantSignupReward = functions.database.ref('/users/{uid}/last_signin_at')
    .onCreate(event => {
      var uid = event.params.uid;
      admin.database().ref(`users/${uid}/referred_by`)
        .once('value').then(function(data) {
          var referred_by_somebody = data.val();
          if (referred_by_somebody) {
            var moneyRef = admin.database()
                .ref(`/users/${uid}/inventory/pieces_of_eight`);
            moneyRef.transaction(function (current_value) {
              return (current_value || 0) + 50;
            });
          }
        });
    });

次に、新しいユーザーが登録した際に、Realtime Database キーを作成してこの関数をトリガーします。たとえば、前の手順で作成した linkWithCredential で関数をトリガーします。

Swift

注: この Firebase プロダクトは、macOS、Mac Catalyst、tvOS、watchOS の各ターゲットでは使用できません。
if let user = Auth.auth().currentUser {
  user.link(with: credential) { (user, error) in
    // Complete any post sign-up tasks here.

    // Trigger the sign-up reward function by creating the "last_signin_at" field.
    // (If this is a value you want to track, you would also update this field in
    // the success listeners of your Firebase Authentication signIn calls.)
    if let user = user {
      let userRecord = Database.database().reference().child("users").child(user.uid)
      userRecord.child("last_signin_at").setValue(ServerValue.timestamp())
    }
  }
}

Kotlin+KTX

Firebase.auth.currentUser!!
        .linkWithCredential(credential)
        .addOnSuccessListener {
            // Complete any post sign-up tasks here.

            // Trigger the sign-up reward function by creating the
            // "last_signin_at" field. (If this is a value you want to track,
            // you would also update this field in the success listeners of
            // your Firebase Authentication signIn calls.)
            val user = Firebase.auth.currentUser!!
            val userRecord = Firebase.database.reference
                    .child("users")
                    .child(user.uid)
            userRecord.child("last_signin_at").setValue(ServerValue.TIMESTAMP)
        }

Java

FirebaseAuth.getInstance().getCurrentUser()
        .linkWithCredential(credential)
        .addOnSuccessListener(new OnSuccessListener<AuthResult>() {
            @Override
            public void onSuccess(AuthResult authResult) {
                // Complete any post sign-up tasks here.

                // Trigger the sign-up reward function by creating the
                // "last_signin_at" field. (If this is a value you want to track,
                // you would also update this field in the success listeners of
                // your Firebase Authentication signIn calls.)
                FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
                DatabaseReference userRecord =
                        FirebaseDatabase.getInstance().getReference()
                                .child("users")
                                .child(user.getUid());
                userRecord.child("last_signin_at").setValue(ServerValue.TIMESTAMP);
            }
        });

受信者がレベル 5 に達したときに紹介者に報酬を付与するには、ユーザー レコードの level フィールドの変更を監視する関数をデプロイします。ユーザーがレベル 4 からレベル 5 に昇格し、ユーザーに紹介者が記録されている場合は、次のように報酬を付与します。

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

exports.rewardReferrals = functions.database.ref('/users/{uid}/level')
    .onUpdate(event => {
      var level = event.data.val();
      var prev_level = event.data.previous.val();
      if (prev_level == 4 && level == 5) {
        var referrerRef = event.data.ref.parent.child('referred_by');
        return referrerRef.once('value').then(function(data) {
          var referrerUid = data.val();
          if (referrerUid) {
            var moneyRef = admin.database()
                .ref(`/users/${referrerUid}/inventory/pieces_of_eight`);
            return moneyRef.transaction(function (current_value) {
              return (current_value || 0) + 50;
            });
          }
        });
      }
    });

これで、紹介者と新しいユーザーの両方に報酬が付与されます。